diff --git a/v2rayN/v2rayN.Desktop/App.axaml b/v2rayN/v2rayN.Desktop/App.axaml
new file mode 100644
index 00000000..d326e4c0
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/App.axaml
@@ -0,0 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/App.axaml.cs b/v2rayN/v2rayN.Desktop/App.axaml.cs
new file mode 100644
index 00000000..5619e7e0
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/App.axaml.cs
@@ -0,0 +1,121 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Markup.Xaml;
+using Splat;
+using v2rayN.Desktop.Common;
+using v2rayN.Desktop.Views;
+
+namespace v2rayN.Desktop;
+
+public partial class App : Application
+{
+ public static EventWaitHandle ProgramStarted;
+ private static Config _config;
+
+ public override void Initialize()
+ {
+ AvaloniaXamlLoader.Load(this);
+ }
+
+ public override void OnFrameworkInitializationCompleted()
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ OnStartup(desktop.Args);
+
+ desktop.Exit += OnExit;
+ desktop.MainWindow = new MainWindow();
+ }
+
+ base.OnFrameworkInitializationCompleted();
+ }
+
+ private void OnStartup(string[]? Args)
+ {
+ var exePathKey = Utils.GetMD5(Utils.GetExePath());
+
+ var rebootas = (Args ?? new string[] { }).Any(t => t == Global.RebootAs);
+ //ProgramStarted = new EventWaitHandle(false, EventResetMode.AutoReset, exePathKey, out bool bCreatedNew);
+ //if (!rebootas && !bCreatedNew)
+ //{
+ // ProgramStarted.Set();
+ // Environment.Exit(0);
+ // return;
+ //}
+
+ Logging.Setup();
+ Init();
+ Logging.LoggingEnabled(_config.guiItem.enableLog);
+ Logging.SaveLog($"v2rayN start up | {Utils.GetVersion()} | {Utils.GetExePath()}");
+ Logging.SaveLog($"{Environment.OSVersion} - {(Environment.Is64BitOperatingSystem ? 64 : 32)}");
+ Logging.ClearLogs();
+
+ Thread.CurrentThread.CurrentUICulture = new(_config.uiItem.currentLanguage);
+ }
+
+ private void Init()
+ {
+ if (ConfigHandler.LoadConfig(ref _config) != 0)
+ {
+ Logging.SaveLog($"Loading GUI configuration file is abnormal,please restart the application{Environment.NewLine}¼ÓÔØGUIÅäÖÃÎļþÒì³£,ÇëÖØÆôÓ¦ÓÃ");
+ Environment.Exit(0);
+ return;
+ }
+ LazyConfig.Instance.SetConfig(_config);
+ Locator.CurrentMutable.RegisterLazySingleton(() => new NoticeHandler(), typeof(NoticeHandler));
+
+ //Under Win10
+ if (Utils.IsWindows() && Environment.OSVersion.Version.Major < 10)
+ {
+ Environment.SetEnvironmentVariable("DOTNET_EnableWriteXorExecute", "0", EnvironmentVariableTarget.User);
+ }
+ }
+
+ private void OnExit(object? sender, ControlledApplicationLifetimeExitEventArgs e)
+ {
+ }
+
+ private void TrayIcon_Clicked(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ if (desktop.MainWindow.IsVisible)
+ {
+ desktop.MainWindow?.Hide();
+ }
+ else
+ {
+ desktop.MainWindow?.Show();
+ }
+ }
+ }
+
+ private void MenuAddServerViaClipboardClick(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ var clipboardData = AvaUtils.GetClipboardData(desktop.MainWindow).Result;
+ Locator.Current.GetService()?.AddServerViaClipboardAsync(clipboardData);
+ }
+ }
+
+ private void MenuSubUpdate_Click(object? sender, EventArgs e)
+ {
+ Locator.Current.GetService()?.UpdateSubscriptionProcess("", false);
+ }
+
+ private void MenuSubUpdateViaProxy_Click(object? sender, EventArgs e)
+ {
+ Locator.Current.GetService()?.UpdateSubscriptionProcess("", true);
+ }
+
+ private void MenuExit_Click(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ Locator.Current.GetService()?.MyAppExitAsync(false);
+
+ desktop.Shutdown();
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico
new file mode 100644
index 00000000..a978e0a8
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon1.ico differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico
new file mode 100644
index 00000000..b625aa8e
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon2.ico differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico
new file mode 100644
index 00000000..6b6db8e6
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/NotifyIcon3.ico differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/add.png b/v2rayN/v2rayN.Desktop/Assets/add.png
new file mode 100644
index 00000000..41170fbe
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/add.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/close.png b/v2rayN/v2rayN.Desktop/Assets/close.png
new file mode 100644
index 00000000..7f25fe11
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/close.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/copy.png b/v2rayN/v2rayN.Desktop/Assets/copy.png
new file mode 100644
index 00000000..b876b511
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/copy.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/delete.png b/v2rayN/v2rayN.Desktop/Assets/delete.png
new file mode 100644
index 00000000..cdcfe3b0
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/delete.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/edit.png b/v2rayN/v2rayN.Desktop/Assets/edit.png
new file mode 100644
index 00000000..f269f777
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/edit.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/fit.png b/v2rayN/v2rayN.Desktop/Assets/fit.png
new file mode 100644
index 00000000..e215fbf6
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/fit.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/light.png b/v2rayN/v2rayN.Desktop/Assets/light.png
new file mode 100644
index 00000000..8a9c5d09
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/light.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/more.png b/v2rayN/v2rayN.Desktop/Assets/more.png
new file mode 100644
index 00000000..93cb6e81
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/more.png differ
diff --git a/v2rayN/v2rayN.Desktop/Assets/refresh.png b/v2rayN/v2rayN.Desktop/Assets/refresh.png
new file mode 100644
index 00000000..88a9dbf1
Binary files /dev/null and b/v2rayN/v2rayN.Desktop/Assets/refresh.png differ
diff --git a/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs
new file mode 100644
index 00000000..5b266748
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Common/AvaUtils.cs
@@ -0,0 +1,64 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform;
+using System.Reflection;
+
+namespace v2rayN.Desktop.Common
+{
+ internal class AvaUtils
+ {
+ public static async Task GetClipboardData(Window owner)
+ {
+ try
+ {
+ var clipboard = TopLevel.GetTopLevel(owner)?.Clipboard;
+ if (clipboard == null) return null;
+ return await clipboard.GetTextAsync();
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ public static async Task SetClipboardData(Visual? visual, string strData)
+ {
+ try
+ {
+ var clipboard = TopLevel.GetTopLevel(visual)?.Clipboard;
+ if (clipboard == null) return;
+ var dataObject = new DataObject();
+ dataObject.Set(DataFormats.Text, strData);
+ await clipboard.SetDataObjectAsync(dataObject);
+ }
+ catch
+ {
+ }
+ }
+
+ public static WindowIcon GetAppIcon(ESysProxyType sysProxyType)
+ {
+ int index = 1;
+ switch (sysProxyType)
+ {
+ case ESysProxyType.ForcedClear:
+ index = 1;
+ break;
+
+ case ESysProxyType.ForcedChange:
+ case ESysProxyType.Pac:
+ index = 2;
+ break;
+
+ case ESysProxyType.Unchanged:
+ index = 3;
+ break;
+ }
+ var uri = new Uri($"avares://{Assembly.GetExecutingAssembly().GetName().Name}/Assets/NotifyIcon{index}.ico");
+ using var bitmap = new Bitmap(AssetLoader.Open(uri));
+ return new(bitmap);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Common/UI.cs b/v2rayN/v2rayN.Desktop/Common/UI.cs
new file mode 100644
index 00000000..0f72c55a
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Common/UI.cs
@@ -0,0 +1,50 @@
+using Avalonia.Controls;
+using Avalonia.Platform.Storage;
+using MsBox.Avalonia;
+using MsBox.Avalonia.Enums;
+
+namespace v2rayN.Desktop.Common
+{
+ internal class UI
+ {
+ private static readonly string caption = Global.AppName;
+
+ public static async Task ShowYesNo(Window owner, string msg)
+ {
+ var box = MessageBoxManager.GetMessageBoxStandard(caption, msg, ButtonEnum.YesNo);
+ return await box.ShowWindowDialogAsync(owner);
+ }
+
+ public static async Task OpenFileDialog(Window owner, FilePickerFileType? filter)
+ {
+ var topLevel = TopLevel.GetTopLevel(owner);
+ if (topLevel == null)
+ {
+ return null;
+ }
+ // Start async operation to open the dialog.
+ var files = await topLevel.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
+ {
+ AllowMultiple = false,
+ FileTypeFilter = filter is null ? [FilePickerFileTypes.All, FilePickerFileTypes.ImagePng] : [filter]
+ });
+
+ return files.FirstOrDefault()?.TryGetLocalPath();
+ }
+
+ public static async Task SaveFileDialog(Window owner, string filter)
+ {
+ var topLevel = TopLevel.GetTopLevel(owner);
+ if (topLevel == null)
+ {
+ return null;
+ }
+ // Start async operation to open the dialog.
+ var files = await topLevel.StorageProvider.SaveFilePickerAsync(new FilePickerSaveOptions
+ {
+ });
+
+ return files?.TryGetLocalPath();
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs
new file mode 100644
index 00000000..e61425f5
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Converters/DelayColorConverter.cs
@@ -0,0 +1,26 @@
+using Avalonia.Data.Converters;
+using Avalonia.Media;
+using System.Globalization;
+
+namespace v2rayN.Desktop.Converters
+{
+ public class DelayColorConverter : IValueConverter
+ {
+ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ int.TryParse(value?.ToString(), out var delay);
+
+ if (delay <= 0)
+ return new SolidColorBrush(Colors.Red);
+ if (delay <= 500)
+ return new SolidColorBrush(Colors.Green);
+ else
+ return new SolidColorBrush(Colors.IndianRed);
+ }
+
+ public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
+ {
+ return null;
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/FodyWeavers.xml b/v2rayN/v2rayN.Desktop/FodyWeavers.xml
new file mode 100644
index 00000000..63fc1484
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/FodyWeavers.xml
@@ -0,0 +1,3 @@
+
+
+
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/GlobalUsings.cs b/v2rayN/v2rayN.Desktop/GlobalUsings.cs
new file mode 100644
index 00000000..bc789ab0
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/GlobalUsings.cs
@@ -0,0 +1,8 @@
+global using ServiceLib;
+global using ServiceLib.Base;
+global using ServiceLib.Common;
+global using ServiceLib.Enums;
+global using ServiceLib.Handler;
+global using ServiceLib.Models;
+global using ServiceLib.Resx;
+global using ServiceLib.ViewModels;
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Handler/SysProxyHandler.cs b/v2rayN/v2rayN.Desktop/Handler/SysProxyHandler.cs
new file mode 100644
index 00000000..55e2082d
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Handler/SysProxyHandler.cs
@@ -0,0 +1,61 @@
+namespace v2rayN.Desktop.Handler
+{
+ public static class SysProxyHandler
+ {
+ public static async Task UpdateSysProxy(Config config, bool forceDisable)
+ {
+ var type = config.systemProxyItem.sysProxyType;
+
+ if (forceDisable && type != ESysProxyType.Unchanged)
+ {
+ type = ESysProxyType.ForcedClear;
+ }
+
+ try
+ {
+ int port = LazyConfig.Instance.GetLocalPort(EInboundProtocol.http);
+ if (port <= 0)
+ {
+ return false;
+ }
+ if (type == ESysProxyType.ForcedChange)
+ {
+ var strProxy = $"{Global.Loopback}:{port}";
+ await SetProxy(strProxy);
+ }
+ else if (type == ESysProxyType.ForcedClear)
+ {
+ await UnsetProxy();
+ }
+ else if (type == ESysProxyType.Unchanged)
+ {
+ }
+ }
+ catch (Exception ex)
+ {
+ Logging.SaveLog(ex.Message, ex);
+ }
+ return true;
+ }
+
+ private static async Task SetProxy(string? strProxy)
+ {
+ await Task.Run(() =>
+ {
+ var httpProxy = strProxy is null ? null : $"{Global.HttpProtocol}{strProxy}";
+ var socksProxy = strProxy is null ? null : $"{Global.SocksProtocol}{strProxy}";
+ var noProxy = $"localhost,127.0.0.0/8,::1";
+
+ Environment.SetEnvironmentVariable("http_proxy", httpProxy, EnvironmentVariableTarget.User);
+ Environment.SetEnvironmentVariable("https_proxy", httpProxy, EnvironmentVariableTarget.User);
+ Environment.SetEnvironmentVariable("all_proxy", socksProxy, EnvironmentVariableTarget.User);
+ Environment.SetEnvironmentVariable("no_proxy", noProxy, EnvironmentVariableTarget.User);
+ });
+ }
+
+ private static async Task UnsetProxy()
+ {
+ await SetProxy(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Program.cs b/v2rayN/v2rayN.Desktop/Program.cs
new file mode 100644
index 00000000..3149aab0
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Program.cs
@@ -0,0 +1,22 @@
+using Avalonia;
+using Avalonia.ReactiveUI;
+
+namespace v2rayN.Desktop;
+
+internal class Program
+{
+ // Initialization code. Don't use any Avalonia, third-party APIs or any
+ // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
+ // yet and stuff might break.
+ [STAThread]
+ public static void Main(string[] args) => BuildAvaloniaApp()
+ .StartWithClassicDesktopLifetime(args);
+
+ // Avalonia configuration, don't remove; also used by visual designer.
+ public static AppBuilder BuildAvaloniaApp()
+ => AppBuilder.Configure()
+ .UsePlatformDetect()
+ .WithInterFont()
+ .LogToTrace()
+ .UseReactiveUI();
+}
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/Styles/GlobalStyles.axaml b/v2rayN/v2rayN.Desktop/Styles/GlobalStyles.axaml
new file mode 100644
index 00000000..ca5f9081
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/Styles/GlobalStyles.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs
new file mode 100644
index 00000000..0794c1b9
--- /dev/null
+++ b/v2rayN/v2rayN.Desktop/ViewModels/ThemeSettingViewModel.cs
@@ -0,0 +1,143 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Styling;
+using ReactiveUI;
+using ReactiveUI.Fody.Helpers;
+using Splat;
+using System.Reactive.Linq;
+
+namespace v2rayN.Desktop.ViewModels
+{
+ public class ThemeSettingViewModel : MyReactiveObject
+ {
+ [Reactive]
+ public bool ColorModeDark { get; set; }
+
+ [Reactive]
+ public int CurrentFontSize { get; set; }
+
+ [Reactive]
+ public string CurrentLanguage { get; set; }
+
+ public ThemeSettingViewModel()
+ {
+ _config = LazyConfig.Instance.Config;
+ _noticeHandler = Locator.Current.GetService();
+
+ BindingUI();
+ RestoreUI();
+ }
+
+ private void RestoreUI()
+ {
+ ModifyTheme(_config.uiItem.colorModeDark);
+ }
+
+ private void BindingUI()
+ {
+ ColorModeDark = _config.uiItem.colorModeDark;
+ CurrentFontSize = _config.uiItem.currentFontSize;
+ CurrentLanguage = _config.uiItem.currentLanguage;
+
+ this.WhenAnyValue(x => x.ColorModeDark)
+ .Subscribe(c =>
+ {
+ if (_config.uiItem.colorModeDark != ColorModeDark)
+ {
+ _config.uiItem.colorModeDark = ColorModeDark;
+ ModifyTheme(ColorModeDark);
+ ConfigHandler.SaveConfig(_config);
+ }
+ });
+
+ this.WhenAnyValue(
+ x => x.CurrentFontSize,
+ y => y > 0)
+ .Subscribe(c =>
+ {
+ if (CurrentFontSize >= Global.MinFontSize)
+ {
+ _config.uiItem.currentFontSize = CurrentFontSize;
+ double size = CurrentFontSize;
+ ModifyFontSize(size);
+
+ ConfigHandler.SaveConfig(_config);
+ }
+ });
+
+ this.WhenAnyValue(
+ x => x.CurrentLanguage,
+ y => y != null && !y.IsNullOrEmpty())
+ .Subscribe(c =>
+ {
+ if (!Utils.IsNullOrEmpty(CurrentLanguage) && _config.uiItem.currentLanguage != CurrentLanguage)
+ {
+ _config.uiItem.currentLanguage = CurrentLanguage;
+ Thread.CurrentThread.CurrentUICulture = new(CurrentLanguage);
+ ConfigHandler.SaveConfig(_config);
+ _noticeHandler?.Enqueue(ResUI.NeedRebootTips);
+ }
+ });
+ }
+
+ private void ModifyTheme(bool isDarkTheme)
+ {
+ var app = Application.Current;
+ if (app is not null)
+ {
+ app.RequestedThemeVariant = isDarkTheme ? ThemeVariant.Dark : ThemeVariant.Light;
+ }
+ }
+
+ private void ModifyFontSize(double size)
+ {
+ Style buttonStyle = new(x => x.OfType