-
-
Notifications
You must be signed in to change notification settings - Fork 11.1k
Select server location from menu bar #8454
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,50 +1,90 @@ | ||||||||||||||||
| import 'dart:io'; | ||||||||||||||||
|
|
||||||||||||||||
| import 'package:lantern/core/models/available_servers.dart'; | ||||||||||||||||
| import 'package:lantern/core/models/macos_extension_state.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'; | ||||||||||||||||
| import 'package:riverpod_annotation/riverpod_annotation.dart'; | ||||||||||||||||
| import 'package:tray_manager/tray_manager.dart'; | ||||||||||||||||
| import 'package:window_manager/window_manager.dart'; | ||||||||||||||||
|
|
||||||||||||||||
| import '../../../core/common/common.dart'; | ||||||||||||||||
| import '../../../core/services/injection_container.dart'; | ||||||||||||||||
| import '../../macos_extension/provider/macos_extension_notifier.dart'; | ||||||||||||||||
| import '../../vpn/provider/server_location_notifier.dart'; | ||||||||||||||||
|
|
||||||||||||||||
| part 'system_tray_notifier.g.dart'; | ||||||||||||||||
|
|
||||||||||||||||
| @Riverpod(keepAlive: true) | ||||||||||||||||
| class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { | ||||||||||||||||
| late VPNStatus _currentStatus; | ||||||||||||||||
| VPNStatus _currentStatus = VPNStatus.disconnected; | ||||||||||||||||
| bool _isUserPro = false; | ||||||||||||||||
| List<Location_> _locations = []; | ||||||||||||||||
|
|
||||||||||||||||
| bool get isConnected => _currentStatus == VPNStatus.connected; | ||||||||||||||||
|
|
||||||||||||||||
| @override | ||||||||||||||||
| Future<void> build() async { | ||||||||||||||||
| if (!PlatformUtils.isDesktop) return; | ||||||||||||||||
| _currentStatus = ref.read(vpnProvider); | ||||||||||||||||
| _initializeState(); | ||||||||||||||||
| _setupListeners(); | ||||||||||||||||
| _setupTrayManager(); | ||||||||||||||||
| await updateTrayMenu(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void _setupTrayManager() { | ||||||||||||||||
| trayManager.addListener(this); | ||||||||||||||||
| ref.onDispose(() => trayManager.removeListener(this)); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void _initializeState() { | ||||||||||||||||
| _currentStatus = ref.read(vpnProvider); | ||||||||||||||||
| _isUserPro = ref.read(isUserProProvider); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void _setupListeners() { | ||||||||||||||||
| _listenToVPNStatus(); | ||||||||||||||||
| _listenToProStatus(); | ||||||||||||||||
| _listenToAvailableServers(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| void _listenToVPNStatus() { | ||||||||||||||||
| ref.listen<VPNStatus>( | ||||||||||||||||
| vpnProvider, | ||||||||||||||||
| (previous, next) async { | ||||||||||||||||
| _currentStatus = next; | ||||||||||||||||
| // Refresh menu on change | ||||||||||||||||
| await updateTrayMenu(); | ||||||||||||||||
| }, | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| _isUserPro = ref.read(isUserProProvider); | ||||||||||||||||
| void _listenToProStatus() { | ||||||||||||||||
| ref.listen<bool>( | ||||||||||||||||
| isUserProProvider, | ||||||||||||||||
| (previous, next) async { | ||||||||||||||||
| _isUserPro = next; | ||||||||||||||||
| await updateTrayMenu(); | ||||||||||||||||
| }, | ||||||||||||||||
| ); | ||||||||||||||||
|
|
||||||||||||||||
| ref.onDispose(() { | ||||||||||||||||
| trayManager.removeListener(this); | ||||||||||||||||
| }); | ||||||||||||||||
|
|
||||||||||||||||
| trayManager.addListener(this); | ||||||||||||||||
| await updateTrayMenu(); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| bool get isConnected => _currentStatus == VPNStatus.connected; | ||||||||||||||||
| void _listenToAvailableServers() { | ||||||||||||||||
| ref.listen<AsyncValue<AvailableServers>>( | ||||||||||||||||
| availableServersProvider, | ||||||||||||||||
| (previous, next) async { | ||||||||||||||||
| final data = next.value; | ||||||||||||||||
| _locations = data?.lantern.locations.values.toList() ?? []; | ||||||||||||||||
| _locations.sort((a, b) { | ||||||||||||||||
| final cmp = a.country.compareTo(b.country); | ||||||||||||||||
| if (cmp != 0) return cmp; | ||||||||||||||||
| return a.city.compareTo(b.city); | ||||||||||||||||
| }); | ||||||||||||||||
| await updateTrayMenu(); | ||||||||||||||||
| }, | ||||||||||||||||
| ); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| Future<void> toggleVPN() async { | ||||||||||||||||
| final notifier = ref.read(vpnProvider.notifier); | ||||||||||||||||
|
|
@@ -55,6 +95,45 @@ 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; | ||||||||||||||||
| } | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| final result = await ref.read(vpnProvider.notifier).connectToServer( | ||||||||||||||||
| ServerLocationType.lanternLocation, | ||||||||||||||||
| location.tag, | ||||||||||||||||
| ); | ||||||||||||||||
| result.fold( | ||||||||||||||||
| (failure) => appLogger | ||||||||||||||||
| .error('Failed to connect: ${failure.localizedErrorMessage}'), | ||||||||||||||||
| (success) { | ||||||||||||||||
| appLogger.info('Connecting to ${location.country} - ${location.city}'); | ||||||||||||||||
| _saveServerLocation(location); | ||||||||||||||||
| }, | ||||||||||||||||
| ); | ||||||||||||||||
|
Comment on lines
115
to
122
|
||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| Future<void> _saveServerLocation(Location_ location) async { | ||||||||||||||||
| final savedServerLocation = | ||||||||||||||||
| sl<LocalStorageService>().getSavedServerLocations(); | ||||||||||||||||
| final serverLocation = savedServerLocation.lanternLocation( | ||||||||||||||||
| server: location, | ||||||||||||||||
| autoSelect: false, | ||||||||||||||||
| ); | ||||||||||||||||
| await ref | ||||||||||||||||
| .read(serverLocationProvider.notifier) | ||||||||||||||||
| .updateServerLocation(serverLocation); | ||||||||||||||||
| } | ||||||||||||||||
|
|
||||||||||||||||
| Future<void> updateTrayMenu() async { | ||||||||||||||||
| final menu = Menu( | ||||||||||||||||
| items: [ | ||||||||||||||||
|
|
@@ -75,6 +154,24 @@ class SystemTrayNotifier extends _$SystemTrayNotifier with TrayListener { | |||||||||||||||
| onClick: (_) => toggleVPN(), | ||||||||||||||||
| ), | ||||||||||||||||
| MenuItem.separator(), | ||||||||||||||||
| if (_isUserPro && _locations.isNotEmpty) | ||||||||||||||||
| MenuItem.submenu( | ||||||||||||||||
| key: 'select_location', | ||||||||||||||||
| label: 'select_location'.i18n, | ||||||||||||||||
| icon: AppImagePaths.nonProfit, | ||||||||||||||||
| 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, | ||||||||||||||||
| onClick: (_) => _onLocationSelected(location), | ||||||||||||||||
|
||||||||||||||||
| onClick: (_) => _onLocationSelected(location), | |
| onClick: (_) { | |
| _onLocationSelected(location).catchError((error, stackTrace) { | |
| stderr.writeln('Error selecting location $displayName: $error'); | |
| stderr.writeln(stackTrace); | |
| }); | |
| }, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The tray path updates the saved/selected server location immediately on
connectToServersuccess. Elsewhere (e.g. server selection UI) the app waits until the VPN status actually becomesconnectedbefore persisting the chosen location, to avoid showing/saving a location if the connection ultimately fails. Consider aligning this flow by updatingserverLocationProvideronly after the VPN reportsconnected(or after verifying the switch completed).