mirror of
https://github.com/2dust/v2rayN.git
synced 2026-04-16 04:25:45 +00:00
Add routing menu to Desktop tray icon
Add routing selection functionality to the system tray menu for macOS/Linux (Avalonia Desktop), bringing feature parity with the Windows version which already has this capability. - Add dynamic routing items to NativeMenu in App.axaml - Implement menu item click handling and state refresh in App.axaml.cs - Support radio-style selection for routing options Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
parent
661affd6a5
commit
01a5805d98
2 changed files with 123 additions and 0 deletions
|
|
@ -64,6 +64,14 @@
|
||||||
IsVisible="{Binding BlSystemProxyPacVisible}"
|
IsVisible="{Binding BlSystemProxyPacVisible}"
|
||||||
ToggleType="Radio" />
|
ToggleType="Radio" />
|
||||||
<NativeMenuItemSeparator />
|
<NativeMenuItemSeparator />
|
||||||
|
<NativeMenuItem
|
||||||
|
Header="{x:Static resx:ResUI.menuRouting}"
|
||||||
|
IsVisible="{Binding BlRouting}">
|
||||||
|
<NativeMenuItem.Menu>
|
||||||
|
<NativeMenu />
|
||||||
|
</NativeMenuItem.Menu>
|
||||||
|
</NativeMenuItem>
|
||||||
|
<NativeMenuItemSeparator />
|
||||||
<NativeMenuItem Click="MenuAddServerViaClipboardClick" Header="{x:Static resx:ResUI.menuAddServerViaClipboard}" />
|
<NativeMenuItem Click="MenuAddServerViaClipboardClick" Header="{x:Static resx:ResUI.menuAddServerViaClipboard}" />
|
||||||
<NativeMenuItem Command="{Binding SubUpdateCmd}" Header="{x:Static resx:ResUI.menuSubUpdate}" />
|
<NativeMenuItem Command="{Binding SubUpdateCmd}" Header="{x:Static resx:ResUI.menuSubUpdate}" />
|
||||||
<NativeMenuItem Command="{Binding SubUpdateViaProxyCmd}" Header="{x:Static resx:ResUI.menuSubUpdateViaProxy}" />
|
<NativeMenuItem Command="{Binding SubUpdateViaProxyCmd}" Header="{x:Static resx:ResUI.menuSubUpdateViaProxy}" />
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,8 @@ namespace v2rayN.Desktop;
|
||||||
|
|
||||||
public partial class App : Application
|
public partial class App : Application
|
||||||
{
|
{
|
||||||
|
private readonly Dictionary<NativeMenuItem, string> _routingMenuMap = [];
|
||||||
|
|
||||||
public override void Initialize()
|
public override void Initialize()
|
||||||
{
|
{
|
||||||
AvaloniaXamlLoader.Load(this);
|
AvaloniaXamlLoader.Load(this);
|
||||||
|
|
@ -20,6 +22,7 @@ public partial class App : Application
|
||||||
{
|
{
|
||||||
AppManager.Instance.InitComponents();
|
AppManager.Instance.InitComponents();
|
||||||
DataContext = StatusBarViewModel.Instance;
|
DataContext = StatusBarViewModel.Instance;
|
||||||
|
InitRoutingMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
desktop.Exit += OnExit;
|
desktop.Exit += OnExit;
|
||||||
|
|
@ -44,6 +47,118 @@ public partial class App : Application
|
||||||
|
|
||||||
private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
|
private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
|
||||||
{
|
{
|
||||||
|
foreach (var menuItem in _routingMenuMap.Keys)
|
||||||
|
{
|
||||||
|
menuItem.Click -= MenuRoutingClick;
|
||||||
|
}
|
||||||
|
_routingMenuMap.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitRoutingMenu()
|
||||||
|
{
|
||||||
|
var vm = StatusBarViewModel.Instance;
|
||||||
|
vm.RoutingItems.CollectionChanged += (_, _) => RefreshRoutingMenuItems();
|
||||||
|
vm.PropertyChanged += (_, args) =>
|
||||||
|
{
|
||||||
|
if (args.PropertyName == nameof(StatusBarViewModel.SelectedRouting))
|
||||||
|
{
|
||||||
|
RefreshRoutingMenuCheckState();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
RefreshRoutingMenuItems();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRoutingMenuItems()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var routingMenu = GetRoutingRootMenu();
|
||||||
|
if (routingMenu == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
routingMenu.Menu ??= new NativeMenu();
|
||||||
|
foreach (var menuItem in _routingMenuMap.Keys)
|
||||||
|
{
|
||||||
|
menuItem.Click -= MenuRoutingClick;
|
||||||
|
}
|
||||||
|
_routingMenuMap.Clear();
|
||||||
|
routingMenu.Menu.Items.Clear();
|
||||||
|
|
||||||
|
foreach (var routing in StatusBarViewModel.Instance.RoutingItems)
|
||||||
|
{
|
||||||
|
var menuItem = new NativeMenuItem
|
||||||
|
{
|
||||||
|
Header = routing.Remarks,
|
||||||
|
IsChecked = routing.IsActive,
|
||||||
|
ToggleType = NativeMenuItemToggleType.Radio,
|
||||||
|
};
|
||||||
|
menuItem.Click += MenuRoutingClick;
|
||||||
|
_routingMenuMap[menuItem] = routing.Id;
|
||||||
|
routingMenu.Menu.Items.Add(menuItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
RefreshRoutingMenuCheckState();
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RefreshRoutingMenuCheckState()
|
||||||
|
{
|
||||||
|
Dispatcher.UIThread.Post(() =>
|
||||||
|
{
|
||||||
|
var selectedRoutingId = StatusBarViewModel.Instance.SelectedRouting?.Id;
|
||||||
|
foreach (var pair in _routingMenuMap)
|
||||||
|
{
|
||||||
|
pair.Key.IsChecked = pair.Value == selectedRoutingId;
|
||||||
|
}
|
||||||
|
}, DispatcherPriority.Background);
|
||||||
|
}
|
||||||
|
|
||||||
|
private NativeMenuItem? GetRoutingRootMenu()
|
||||||
|
{
|
||||||
|
if (Current == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var icons = TrayIcon.GetIcons(Current);
|
||||||
|
if (icons.Count == 0 || icons[0].Menu is not NativeMenu trayMenu)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return trayMenu.Items
|
||||||
|
.OfType<NativeMenuItem>()
|
||||||
|
.FirstOrDefault(x => Equals(x.Header, ResUI.menuRouting));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MenuRoutingClick(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (sender is not NativeMenuItem menuItem)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_routingMenuMap.TryGetValue(menuItem, out var routingId))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var target = StatusBarViewModel.Instance.RoutingItems.FirstOrDefault(x => x.Id == routingId);
|
||||||
|
if (target == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (StatusBarViewModel.Instance.SelectedRouting?.Id == target.Id)
|
||||||
|
{
|
||||||
|
RefreshRoutingMenuCheckState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusBarViewModel.Instance.SelectedRouting = target;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e)
|
private async void MenuAddServerViaClipboardClick(object? sender, EventArgs e)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue