Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions lib/core/common/app_build_info.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,8 @@ class AppBuildInfo {
);
}

///Always use values from app build info this will ensure that the version and build number are same
Future<String> resolveAppVersionLabel() async {
if(AppBuildInfo.buildType=='production'){
/// always use value from pubspec for production builds
final info = await PackageInfo.fromPlatform();
return '${info.version} (${info.buildNumber})';
}
if (AppBuildInfo.version.isNotEmpty) return AppBuildInfo.version;
final info = await PackageInfo.fromPlatform();
return '${info.version} (${info.buildNumber})';
}
2 changes: 1 addition & 1 deletion lib/features/home/provider/home_notifier.g.dart

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

190 changes: 168 additions & 22 deletions lib/features/system_tray/provider/system_tray_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import 'dart:io';

import 'package:lantern/core/models/available_servers.dart';
import 'package:lantern/core/models/entity/app_setting_entity.dart';
import 'package:lantern/core/models/entity/server_location_entity.dart';
import 'package:lantern/core/models/macos_extension_state.dart';
import 'package:lantern/features/home/provider/app_setting_notifier.dart';
import 'package:lantern/features/vpn/provider/available_servers_notifier.dart';
import 'package:lantern/features/vpn/provider/vpn_notifier.dart';
import 'package:lantern/features/window/provider/window_notifier.dart';
Expand All @@ -21,9 +24,15 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
VPNStatus _currentStatus = VPNStatus.disconnected;
bool _isUserPro = false;
List<Location_> _locations = [];
RoutingMode _currentRoutingMode = RoutingMode.full;
ServerLocationEntity? _serverLocation;

bool get isConnected => _currentStatus == VPNStatus.connected;

bool get _isAutoLocation =>
_serverLocation?.serverType.toServerLocationType ==
ServerLocationType.auto;

@override
Future<void> build() async {
if (!PlatformUtils.isDesktop) return;
Expand All @@ -42,12 +51,16 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
void _initializeState() {
_currentStatus = ref.read(vpnProvider);
_isUserPro = ref.read(isUserProProvider);
_currentRoutingMode = ref.read(appSettingProvider).routingMode;
_serverLocation = ref.read(serverLocationProvider);
}

void _setupListeners() {
_listenToVPNStatus();
_listenToProStatus();
_listenToAvailableServers();
_listenToServerLocation();
_listenToRoutingMode();
}

void _listenToVPNStatus() {
Expand Down Expand Up @@ -86,6 +99,28 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
);
}

void _listenToServerLocation() {
ref.listen<ServerLocationEntity>(
serverLocationProvider,
(previous, next) async {
_serverLocation = next;
await updateTrayMenu();
},
);
}

void _listenToRoutingMode() {
ref.listen<AppSetting>(
appSettingProvider,
(previous, next) async {
if (previous?.routingMode != next.routingMode) {
_currentRoutingMode = next.routingMode;
await updateTrayMenu();
}
},
);
}

Future<void> toggleVPN() async {
final notifier = ref.read(vpnProvider.notifier);
if (_currentStatus == VPNStatus.connected) {
Expand All @@ -97,16 +132,7 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {

/// Handle location selection from tray menu
Future<void> _onLocationSelected(Location_ location) async {
/// Check if extension is installed and up to date before connecting
if (PlatformUtils.isMacOS) {
final systemExtensionStatus = ref.read(macosExtensionProvider);
if (systemExtensionStatus.status != SystemExtensionStatus.installed &&
systemExtensionStatus.status != SystemExtensionStatus.activated) {
windowManager.show();
appRouter.push(const MacOSExtensionDialog());
return;
}
}
if (!_checkMacOSExtension()) return;

final result = await ref.read(vpnProvider.notifier).connectToServer(
ServerLocationType.lanternLocation,
Expand All @@ -122,6 +148,35 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
);
}

/// Handle smart location selection from tray menu
Future<void> _onSmartLocationSelected() async {
if (!_checkMacOSExtension()) return;

await ref
.read(serverLocationProvider.notifier)
.updateServerLocation(initialServerLocation());
await ref.read(vpnProvider.notifier).startVPN(force: true);
}

/// Handle routing mode selection from tray menu
Future<void> _onRoutingModeSelected(RoutingMode mode) async {
await ref.read(appSettingProvider.notifier).setRoutingMode(mode);
}

/// Returns true if OK to proceed, false if blocked by missing extension
bool _checkMacOSExtension() {
if (PlatformUtils.isMacOS) {
final systemExtensionStatus = ref.read(macosExtensionProvider);
if (systemExtensionStatus.status != SystemExtensionStatus.installed &&
systemExtensionStatus.status != SystemExtensionStatus.activated) {
windowManager.show();
appRouter.push(const MacOSExtensionDialog());
return false;
}
}
return true;
}

Future<void> _saveServerLocation(Location_ location) async {
final savedServerLocation =
sl<LocalStorageService>().getSavedServerLocations();
Expand All @@ -134,16 +189,59 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
.updateServerLocation(serverLocation);
}

/// Build the current location display string (flag emoji + city)
/// shown when connected
String get _currentLocationDisplay {
try {
if (_serverLocation == null) return '';

final loc = _serverLocation!;
String countryCode = '';
String displayName = '';

if (loc.serverType.toServerLocationType == ServerLocationType.auto) {
/// For auto location, we use the autoLocation info which contains the actual connected server details
final auto_ = loc.autoLocation!;
countryCode = auto_.countryCode;
displayName = auto_.displayName;
} else {
countryCode = loc.countryCode;
displayName = loc.displayName;
}

if (displayName.isEmpty) return '';

final flag = _countryCodeToFlagEmoji(countryCode);
return flag.isNotEmpty ? '$flag $displayName' : displayName;
} catch (e) {
appLogger.error('Error building location display', e);
return '';
}
}

Future<void> updateTrayMenu() async {
final locationDisplay = _currentLocationDisplay;

final menu = Menu(
items: [
MenuItem.separator(),
// Status: Connected / Disconnected (greyed out, non-clickable)
MenuItem(
key: 'status_label',
disabled: true,
label: _currentStatus == VPNStatus.connected
? 'status_on'.i18n
: 'status_off'.i18n,
),

if (isConnected && locationDisplay.isNotEmpty)
MenuItem(
key: 'current_location',
disabled: true,
label: locationDisplay,
),
MenuItem.separator(),

MenuItem(
key: 'toggle',
label: _currentStatus == VPNStatus.connected
Expand All @@ -154,24 +252,58 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
onClick: (_) => toggleVPN(),
),
MenuItem.separator(),

if (_isUserPro && _locations.isNotEmpty)
MenuItem.submenu(
key: 'select_location',
label: 'select_location'.i18n,
disabled: _currentStatus == VPNStatus.connecting ||
_currentStatus == VPNStatus.disconnecting,
submenu: Menu(
items: _locations.map((location) {
final displayName = location.city.isNotEmpty
? '${location.country} - ${location.city}'
: location.country;
return MenuItem(
key: 'location_${location.tag}',
label: displayName,
icon: AppImagePaths.safeFlagPath(location.countryCode),
onClick: (_) => _onLocationSelected(location),
);
}).toList(),
items: [
// Smart Location as first option with checkmark
MenuItem.checkbox(
key: 'smart_location',
label: 'smart_location'.i18n,
checked: _isAutoLocation,
onClick: (_) => _onSmartLocationSelected(),
),
MenuItem.separator(),
// Server list
..._locations.map((location) {
final displayName = location.city.isNotEmpty
? '${location.country} - ${location.city}'
: location.country;
return MenuItem(
key: 'location_${location.tag}',
label: displayName,
icon: AppImagePaths.safeFlagPath(location.countryCode),
onClick: (_) => _onLocationSelected(location),
);
}),
],
),
),
MenuItem.submenu(
key: 'routing_mode',
label: 'routing_mode'.i18n,
submenu: Menu(
items: [
MenuItem.checkbox(
key: 'smart_routing',
label: 'smart_routing'.i18n,
checked: _currentRoutingMode == RoutingMode.smart,
onClick: (_) => _onRoutingModeSelected(RoutingMode.smart),
),
MenuItem.checkbox(
key: 'full_tunnel',
label: 'full_tunnel'.i18n,
checked: _currentRoutingMode == RoutingMode.full,
onClick: (_) => _onRoutingModeSelected(RoutingMode.full),
),
],
),
),
if (!_isUserPro)
MenuItem(
key: 'upgrade_to_pro',
Expand All @@ -186,7 +318,6 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
key: 'join_server',
label: 'join_server'.i18n,
onClick: (_) {
// Open Lantern and navigate to the join server page
ref.read(windowProvider.notifier).open(focus: true);
appRouter.push(JoinPrivateServer());
},
Expand Down Expand Up @@ -247,3 +378,18 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener {
await trayManager.popUpContextMenu();
}
}

/// Converts a 2-letter ISO country code to a flag emoji
/// e.g. "US" → "🇺🇸", "GB" → "🇬🇧"
String _countryCodeToFlagEmoji(String countryCode) {
final code = countryCode.toUpperCase();
if (code.length != 2) return '';
// Ensure both characters are ASCII letters A–Z before computing the emoji.
final isAsciiLetters = code.codeUnits.every(
(c) => c >= 0x41 && c <= 0x5A,
);
if (!isAsciiLetters) return '';
return String.fromCharCodes(
code.codeUnits.map((c) => c - 0x41 + 0x1F1E6),
);
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

42 changes: 21 additions & 21 deletions macos/Podfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -135,32 +135,32 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos

SPEC CHECKSUMS:
app_links: 05a6ec2341985eb05e9f97dc63f5837c39895c3f
auto_updater_macos: 3a42f1a06be6981f1a18be37e6e7bf86aa732118
desktop_webview_window: 7e37af677d6d19294cb433d9b1d878ef78dffa4d
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
flutter_inappwebview_macos: c2d68649f9f8f1831bfcd98d73fd6256366d9d1d
flutter_local_notifications: 4bf37a31afde695b56091b4ae3e4d9c7a7e6cda0
flutter_timezone: d272288c69082ad571630e0d17140b3d6b93dc0c
app_links: c3185399a5cabc2e610ee5ad52fb7269b84ff869
auto_updater_macos: 3e3462c418fe4e731917eacd8d28eef7af84086d
desktop_webview_window: d4365e71bcd4e1aa0c14cf0377aa24db0c16a7e2
device_info_plus: 1b14eed9bf95428983aed283a8d51cce3d8c4215
flutter_inappwebview_macos: bdf207b8f4ebd58e86ae06cd96b147de99a67c9b
flutter_local_notifications: 4ccab5b7a22835214a6672e3f9c5e8ae207dab36
flutter_timezone: b3bc0c587d8780d395651284a1ff46eb1e5753ac
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
in_app_purchase_storekit: d1a48cb0f8b29dbf5f85f782f5dd79b21b90a5e6
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
in_app_purchase_storekit: a1ce04056e23eecc666b086040239da7619cd783
mobile_scanner: 77265f3dc8d580810e91849d4a0811a90467ed5e
ObjectBox: a60820ec1c903702350585d8cc99c1268c99e688
objectbox_flutter_libs: 013bbc27a57c5120e1408c0b33386c78cd0b5847
objective_c: ec13431e45ba099cb734eb2829a5c1cd37986cba
objectbox_flutter_libs: fdba3af52a244037417fe170952a3dd819fee1af
objective_c: e5f8194456e8fc943e034d1af00510a1bc29c067
OrderedSet: e539b66b644ff081c73a262d24ad552a69be3a94
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 0b743cbb62d8e47eab856f09262bb8c1ddcfe6ba
screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
Sentry: b53951377b78e21a734f5dc8318e333dbfc682d7
sentry_flutter: 4c33648b7e83310aa1fdb1b10c5491027d9643f0
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
sentry_flutter: f074f75557daea0e1dd9607381a05cc0e3e456fe
share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: 5086985c1d43c5ba4d5e69a4e8083a389e2909e6
Sparkle: a346a4341537c625955751ed3ae4b340b68551fa
store_checker: 0b5120a0f0a0f36af7b4be01a049e6006151dc16
tray_manager: a104b5c81b578d83f3c3d0f40a997c8b10810166
url_launcher_macos: f87a979182d112f911de6820aefddaf56ee9fbfd
window_manager: b729e31d38fb04905235df9ea896128991cad99e
store_checker: 387169de0dffe57b6370d54cc027e9f95051b57f
tray_manager: 9064e219c56d75c476e46b9a21182087930baf90
url_launcher_macos: 175a54c831f4375a6cf895875f716ee5af3888ce
window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575

PODFILE CHECKSUM: b1a5a5e815560a377d8b3e64b4f3f97e2bd372ba

Expand Down
Loading