Compare commits

..

23 commits

Author SHA1 Message Date
DHR60
11924b545f PreCheck 2025-09-23 16:50:17 +08:00
DHR60
ab1dc45ed4 Fix 2025-09-23 16:49:52 +08:00
DHR60
2d41272659 Avoid self-reference 2025-09-23 12:46:59 +08:00
DHR60
e7f75010d3 Add chain selection control to group outbounds 2025-09-23 11:24:27 +08:00
DHR60
aa1ccdd01b Refactor 2025-09-23 11:24:17 +08:00
DHR60
b17323c982 Add helper function 2025-09-23 11:11:03 +08:00
DHR60
71dcd8d1de Adjust chained proxy, actual outbound is at the top
Based on actual network flow instead of data packets
2025-09-21 17:01:41 +08:00
DHR60
dace865e6c Refactor 2025-09-21 17:01:41 +08:00
DHR60
fad94e68d2 Avoid duplicate tags 2025-09-21 17:01:41 +08:00
DHR60
c116aae242 Add group in traffic splitting support 2025-09-21 17:01:41 +08:00
DHR60
ff1c9093a2 Add PolicyGroup include other Group support 2025-09-21 17:01:41 +08:00
DHR60
8bb20c0ab8 Add fallback support 2025-09-21 17:01:41 +08:00
DHR60
17a3a516c7 Fix 2025-09-21 17:01:41 +08:00
DHR60
8f0d7e54d8 Add Proxy Chain support 2025-09-21 17:01:41 +08:00
DHR60
2ab79afa02 Adjust UI 2025-09-21 17:01:41 +08:00
DHR60
e29d292732 Add generate policy group 2025-09-21 17:01:41 +08:00
DHR60
9ef228db1e Add Policy Group support 2025-09-21 17:01:41 +08:00
DHR60
34327532e6 Rename 2025-09-21 17:01:41 +08:00
DHR60
8af6eda165 Exclude specific profile types from selection 2025-09-21 17:01:41 +08:00
DHR60
f979d13109 Fix right click not working 2025-09-21 17:01:41 +08:00
DHR60
6166b6c0e3 avalonia 2025-09-21 17:01:40 +08:00
DHR60
8c094dd976 VM and wpf 2025-09-21 17:01:40 +08:00
DHR60
5c4f485471 Multi Profile 2025-09-21 17:01:40 +08:00
47 changed files with 479 additions and 745 deletions

View file

@ -22,7 +22,7 @@ jobs:
matrix:
configuration: [Release]
runs-on: ubuntu-24.04
runs-on: ubuntu-22.04
steps:
- name: Checkout

View file

@ -1,7 +1,7 @@
<Project>
<PropertyGroup>
<Version>7.15.0</Version>
<Version>7.14.12</Version>
</PropertyGroup>
<PropertyGroup>

View file

@ -5,7 +5,6 @@
<CentralPackageVersionOverrideEnabled>false</CentralPackageVersionOverrideEnabled>
</PropertyGroup>
<ItemGroup>
<PackageVersion Include="Avalonia.AvaloniaEdit" Version="11.3.0" />
<PackageVersion Include="Avalonia.Controls.DataGrid" Version="11.3.6" />
<PackageVersion Include="Avalonia.Desktop" Version="11.3.6" />
<PackageVersion Include="Avalonia.Diagnostics" Version="11.3.6" />
@ -20,9 +19,8 @@
<PackageVersion Include="ReactiveUI.Fody" Version="19.5.41" />
<PackageVersion Include="ReactiveUI.WPF" Version="20.4.1" />
<PackageVersion Include="Semi.Avalonia" Version="11.2.1.10" />
<PackageVersion Include="Semi.Avalonia.AvaloniaEdit" Version="11.2.0.1" />
<PackageVersion Include="Semi.Avalonia.DataGrid" Version="11.2.1.10" />
<PackageVersion Include="NLog" Version="6.0.4" />
<PackageVersion Include="Splat.NLog" Version="16.2.1" />
<PackageVersion Include="sqlite-net-pcl" Version="1.9.172" />
<PackageVersion Include="TaskScheduler" Version="2.12.2" />
<PackageVersion Include="WebDav.Client" Version="2.9.0" />

View file

@ -12,6 +12,7 @@ public enum EViewAction
ProfilesFocus,
ShareSub,
ShareServer,
ShowHideWindow,
ScanScreenTask,
ScanImageTask,
BrowseServer,

View file

@ -1,32 +0,0 @@
using System.Reactive;
namespace ServiceLib.Events;
public static class AppEvents
{
public static readonly EventChannel<Unit> ReloadRequested = new();
public static readonly EventChannel<bool?> ShowHideWindowRequested = new();
public static readonly EventChannel<Unit> AddServerViaScanRequested = new();
public static readonly EventChannel<Unit> AddServerViaClipboardRequested = new();
public static readonly EventChannel<bool> SubscriptionsUpdateRequested = new();
public static readonly EventChannel<Unit> ProfilesRefreshRequested = new();
public static readonly EventChannel<Unit> SubscriptionsRefreshRequested = new();
public static readonly EventChannel<Unit> ProxiesReloadRequested = new();
public static readonly EventChannel<ServerSpeedItem> DispatcherStatisticsRequested = new();
public static readonly EventChannel<string> SendSnackMsgRequested = new();
public static readonly EventChannel<string> SendMsgViewRequested = new();
public static readonly EventChannel<Unit> AppExitRequested = new();
public static readonly EventChannel<bool> ShutdownRequested = new();
public static readonly EventChannel<Unit> AdjustMainLvColWidthRequested = new();
public static readonly EventChannel<string> SetDefaultServerRequested = new();
public static readonly EventChannel<Unit> RoutingsMenuRefreshRequested = new();
public static readonly EventChannel<Unit> TestServerRequested = new();
public static readonly EventChannel<Unit> InboundDisplayRequested = new();
public static readonly EventChannel<ESysProxyType> SysProxyChangeRequested = new();
}

View file

@ -1,29 +0,0 @@
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
namespace ServiceLib.Events;
public sealed class EventChannel<T>
{
private readonly ISubject<T> _subject = Subject.Synchronize(new Subject<T>());
public IObservable<T> AsObservable()
{
return _subject.AsObservable();
}
public void Publish(T value)
{
_subject.OnNext(value);
}
public void Publish()
{
if (typeof(T) != typeof(Unit))
{
throw new InvalidOperationException("Publish() without value is only valid for EventChannel<Unit>.");
}
_subject.OnNext((T)(object)Unit.Default);
}
}

View file

@ -450,14 +450,6 @@ public class Global
"none"
];
public static readonly Dictionary<string, string> LogLevelColors = new()
{
{ "debug", "#6C757D" },
{ "info", "#2ECC71" },
{ "warning", "#FFA500" },
{ "error", "#E74C3C" },
};
public static readonly List<string> InboundTags =
[
"socks",

View file

@ -1,7 +1,6 @@
global using ServiceLib.Base;
global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Helper;
global using ServiceLib.Manager;

View file

@ -0,0 +1,21 @@
using System.Reactive;
using System.Reactive.Subjects;
namespace ServiceLib.Handler;
public static class AppEvents
{
public static readonly Subject<Unit> ProfilesRefreshRequested = new();
public static readonly Subject<string> SendSnackMsgRequested = new();
public static readonly Subject<string> SendMsgViewRequested = new();
public static readonly Subject<Unit> AppExitRequested = new();
public static readonly Subject<bool> ShutdownRequested = new();
public static readonly Subject<Unit> AdjustMainLvColWidthRequested = new();
public static readonly Subject<ServerSpeedItem> DispatcherStatisticsRequested = new();
}

View file

@ -1253,49 +1253,12 @@ public static class ConfigHandler
ProfileItem? itemSocks = null;
if (node.ConfigType != EConfigType.Custom && coreType != ECoreType.sing_box && config.TunModeItem.EnableTun)
{
var tun2SocksAddress = node.Address;
if (node.ConfigType > EConfigType.Group)
{
static async Task<List<string>> GetChildNodeAddressesAsync(string parentIndexId)
{
var childAddresses = new List<string>();
if (!ProfileGroupItemManager.Instance.TryGet(parentIndexId, out var groupItem) || groupItem.ChildItems.IsNullOrEmpty())
return childAddresses;
var childIds = Utils.String2List(groupItem.ChildItems);
foreach (var childId in childIds)
{
var childNode = await AppManager.Instance.GetProfileItem(childId);
if (childNode == null)
continue;
if (!childNode.IsComplex())
{
childAddresses.Add(childNode.Address);
}
else if (childNode.ConfigType > EConfigType.Group)
{
var subAddresses = await GetChildNodeAddressesAsync(childNode.IndexId);
childAddresses.AddRange(subAddresses);
}
}
return childAddresses;
}
var lstAddresses = await GetChildNodeAddressesAsync(node.IndexId);
if (lstAddresses.Count > 0)
{
tun2SocksAddress = Utils.List2String(lstAddresses);
}
}
itemSocks = new ProfileItem()
{
CoreType = ECoreType.sing_box,
ConfigType = EConfigType.SOCKS,
Address = Global.Loopback,
SpiderX = tun2SocksAddress, // Tun2SocksAddress
SpiderX = node.Address, // Tun2SocksAddress
Port = AppManager.Instance.GetLocalPort(EInboundProtocol.socks)
};
}

View file

@ -1,3 +1,5 @@
using System.Reactive;
namespace ServiceLib.Manager;
public sealed class AppManager
@ -95,7 +97,7 @@ public sealed class AppManager
Logging.SaveLog("AppExitAsync Begin");
await SysProxyHandler.UpdateSysProxy(_config, true);
AppEvents.AppExitRequested.Publish();
AppEvents.AppExitRequested.OnNext(Unit.Default);
await Task.Delay(50); //Wait for AppExitRequested to be processed
await ConfigHandler.SaveConfig(_config);
@ -119,13 +121,7 @@ public sealed class AppManager
public void Shutdown(bool byUser)
{
AppEvents.ShutdownRequested.Publish(byUser);
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await AppManager.Instance.AppExitAsync(true);
AppEvents.ShutdownRequested.OnNext(byUser);
}
#endregion App

View file

@ -11,7 +11,7 @@ public class NoticeManager
{
return;
}
AppEvents.SendSnackMsgRequested.Publish(content);
AppEvents.SendSnackMsgRequested.OnNext(content);
}
public void SendMessage(string? content)
@ -20,7 +20,7 @@ public class NoticeManager
{
return;
}
AppEvents.SendMsgViewRequested.Publish(content);
AppEvents.SendMsgViewRequested.OnNext(content);
}
public void SendMessageEx(string? content)

View file

@ -109,42 +109,6 @@ public class ProfileItem : ReactiveObject
return true;
}
public async Task<bool> HasCycle(HashSet<string> visited, HashSet<string> stack)
{
if (ConfigType < EConfigType.Group)
return false;
if (stack.Contains(IndexId))
return true;
if (visited.Contains(IndexId))
return false;
visited.Add(IndexId);
stack.Add(IndexId);
if (ProfileGroupItemManager.Instance.TryGet(IndexId, out var group)
&& !group.ChildItems.IsNullOrEmpty())
{
var childProfiles = (await Task.WhenAll(
Utils.String2List(group.ChildItems)
.Where(p => !p.IsNullOrEmpty())
.Select(AppManager.Instance.GetProfileItem)
))
.Where(p => p != null)
.ToList();
foreach (var child in childProfiles)
{
if (await child.HasCycle(visited, stack))
return true;
}
}
stack.Remove(IndexId);
return false;
}
#endregion function
[PrimaryKey]

File diff suppressed because it is too large Load diff

View file

@ -11,7 +11,7 @@
</PackageReference>
<PackageReference Include="ReactiveUI.Fody" />
<PackageReference Include="sqlite-net-pcl" />
<PackageReference Include="NLog" />
<PackageReference Include="Splat.NLog" />
<PackageReference Include="WebDav.Client" />
<PackageReference Include="YamlDotNet" />
<PackageReference Include="QRCoder" />

View file

@ -123,14 +123,7 @@ public class ActionPrecheckService(Config config)
errors.Add(string.Format(ResUI.GroupEmpty, item.Remarks));
return errors;
}
var hasCycle = await item.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
errors.Add(string.Format(ResUI.GroupSelfReference, item.Remarks));
return errors;
}
foreach (var child in Utils.String2List(group.ChildItems))
{
var childErrors = new List<string>();
@ -146,6 +139,13 @@ public class ActionPrecheckService(Config config)
continue;
}
// self-reference check
if (childItem.IndexId == item.IndexId)
{
childErrors.Add(string.Format(ResUI.GroupSelfReference, childItem.Remarks));
continue;
}
if (childItem.ConfigType is EConfigType.Custom or EConfigType.ProxyChain)
{
childErrors.Add(string.Format(ResUI.InvalidProperty, childItem.Remarks));

View file

@ -79,7 +79,6 @@ public class CoreConfigClashService
//external-controller
fileContent["external-controller"] = $"{Global.Loopback}:{AppManager.Instance.StatePort2}";
fileContent.Remove("secret");
//allow-lan
if (_config.Inbound.First().AllowLANConn)
{

View file

@ -414,19 +414,16 @@ public partial class CoreConfigSingboxService
return 0;
}
List<string> domain = new();
var domain = string.Empty;
if (Utils.IsDomain(node.Address)) // normal outbound
{
domain.Add(node.Address);
domain = node.Address;
}
if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty()) // Tun2SocksAddress
else if (node.Address == Global.Loopback && node.SpiderX.IsNotEmpty() && Utils.IsDomain(node.SpiderX)) // Tun2SocksAddress
{
domain.AddRange(Utils.String2List(node.SpiderX)
.Where(Utils.IsDomain)
.Distinct()
.ToList());
domain = node.SpiderX;
}
if (domain.Count == 0)
if (domain.IsNullOrEmpty())
{
return 0;
}
@ -435,7 +432,7 @@ public partial class CoreConfigSingboxService
singboxConfig.dns.rules.Insert(0, new Rule4Sbox
{
server = server,
domain = domain,
domain = [domain],
});
return await Task.FromResult(0);

View file

@ -217,15 +217,9 @@ public partial class CoreConfigSingboxService
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
return -1;
}
// remove custom nodes
// remove group nodes for proxy chain
// avoid self-reference
var childProfiles = (await Task.WhenAll(
Utils.String2List(profileGroupItem.ChildItems)
.Where(p => !p.IsNullOrEmpty())
@ -236,6 +230,7 @@ public partial class CoreConfigSingboxService
&& p.IsValid()
&& p.ConfigType != EConfigType.Custom
&& (node.ConfigType == EConfigType.PolicyGroup || p.ConfigType < EConfigType.Group)
&& p.IndexId != node.IndexId
)
.ToList();

View file

@ -493,13 +493,6 @@ public partial class CoreConfigV2rayService
{
return -1;
}
var hasCycle = await node.HasCycle(new HashSet<string>(), new HashSet<string>());
if (hasCycle)
{
return -1;
}
// remove custom nodes
// remove group nodes for proxy chain
var childProfiles = (await Task.WhenAll(

View file

@ -2,9 +2,11 @@ using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Runtime.InteropServices;
using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -223,11 +225,11 @@ public class CheckUpdateViewModel : MyReactiveObject
{
if (blReload)
{
AppEvents.ReloadRequested.Publish();
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
}
else
{
await CoreManager.Instance.CoreStop();
Locator.Current.GetService<MainWindowViewModel>()?.CloseCore();
}
}
@ -338,6 +340,6 @@ public class CheckUpdateViewModel : MyReactiveObject
{
return;
}
found.Remarks = model.Remarks;
found.Remarks = model.Remarks;
}
}

View file

@ -69,8 +69,6 @@ public class ClashProxiesViewModel : MyReactiveObject
SortingSelected = _config.ClashUIItem.ProxiesSorting;
RuleModeSelected = (int)_config.ClashUIItem.RuleMode;
#region WhenAnyValue && ReactiveCommand
this.WhenAnyValue(
x => x.SelectedGroup,
y => y != null && y.Name.IsNotEmpty())
@ -91,17 +89,6 @@ public class ClashProxiesViewModel : MyReactiveObject
y => y == true)
.Subscribe(c => { _config.ClashUIItem.ProxiesAutoRefresh = AutoRefresh; });
#endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ProxiesReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await ProxiesReload());
#endregion AppEvents
_ = Init();
}

View file

@ -1,8 +1,8 @@
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Linq;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -194,7 +194,7 @@ public class MainWindowViewModel : MyReactiveObject
});
RebootAsAdminCmd = ReactiveCommand.CreateFromTask(async () =>
{
await AppManager.Instance.RebootAsAdmin();
await RebootAsAdmin();
});
ClearServerStatisticsCmd = ReactiveCommand.CreateFromTask(async () =>
{
@ -227,30 +227,6 @@ public class MainWindowViewModel : MyReactiveObject
#endregion WhenAnyValue && ReactiveCommand
#region AppEvents
AppEvents.ReloadRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await Reload());
AppEvents.AddServerViaScanRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaScanAsync());
AppEvents.AddServerViaClipboardRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await AddServerViaClipboardAsync(null));
AppEvents.SubscriptionsUpdateRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async blProxy => await UpdateSubscriptionProcess("", blProxy));
#endregion AppEvents
_ = Init();
}
@ -258,7 +234,7 @@ public class MainWindowViewModel : MyReactiveObject
{
_config.UiItem.ShowInTaskbar = true;
//await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinRouting(_config);
await ConfigHandler.InitBuiltinDNS(_config);
await ConfigHandler.InitBuiltinFullConfigTemplate(_config);
await ProfileExManager.Instance.Init();
@ -275,6 +251,7 @@ public class MainWindowViewModel : MyReactiveObject
BlReloadEnabled = true;
await Reload();
await AutoHideStartup();
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
}
#endregion Init
@ -303,7 +280,7 @@ public class MainWindowViewModel : MyReactiveObject
}
if (_config.UiItem.EnableAutoAdjustMainLvColWidth)
{
AppEvents.AdjustMainLvColWidthRequested.Publish();
AppEvents.AdjustMainLvColWidthRequested.OnNext(Unit.Default);
}
}
}
@ -314,7 +291,12 @@ public class MainWindowViewModel : MyReactiveObject
{
return;
}
AppEvents.DispatcherStatisticsRequested.Publish(update);
AppEvents.DispatcherStatisticsRequested.OnNext(update);
}
public void ShowHideWindow(bool? blShow)
{
_updateView?.Invoke(EViewAction.ShowHideWindow, blShow);
}
#endregion Actions
@ -323,14 +305,14 @@ public class MainWindowViewModel : MyReactiveObject
private async Task RefreshServers()
{
AppEvents.ProfilesRefreshRequested.Publish();
AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default);
await Task.Delay(200);
}
private void RefreshSubscriptions()
{
AppEvents.SubscriptionsRefreshRequested.Publish();
Locator.Current.GetService<ProfilesViewModel>()?.RefreshSubscriptions();
}
#endregion Servers && Groups
@ -466,7 +448,7 @@ public class MainWindowViewModel : MyReactiveObject
var ret = await _updateView?.Invoke(EViewAction.OptionSettingWindow, null);
if (ret == true)
{
AppEvents.InboundDisplayRequested.Publish();
Locator.Current.GetService<StatusBarViewModel>()?.InboundDisplayStatus();
await Reload();
}
}
@ -477,7 +459,7 @@ public class MainWindowViewModel : MyReactiveObject
if (ret == true)
{
await ConfigHandler.InitBuiltinRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish();
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
await Reload();
}
}
@ -500,6 +482,12 @@ public class MainWindowViewModel : MyReactiveObject
}
}
public async Task RebootAsAdmin()
{
ProcUtils.RebootAsAdmin();
await AppManager.Instance.AppExitAsync(true);
}
private async Task ClearServerStatistics()
{
await StatisticsManager.Instance.ClearAllServerStatistics();
@ -545,15 +533,9 @@ public class MainWindowViewModel : MyReactiveObject
await SysProxyHandler.UpdateSysProxy(_config, false);
await Task.Delay(1000);
});
AppEvents.TestServerRequested.Publish();
Locator.Current.GetService<StatusBarViewModel>()?.TestServerAvailability();
var showClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (showClashUI)
{
AppEvents.ProxiesReloadRequested.Publish();
}
RxApp.MainThreadScheduler.Schedule(() => ReloadResult(showClashUI));
RxApp.MainThreadScheduler.Schedule(() => _ = ReloadResult());
BlReloadEnabled = true;
if (_hasNextReloadJob)
@ -563,11 +545,19 @@ public class MainWindowViewModel : MyReactiveObject
}
}
private void ReloadResult(bool showClashUI)
public async Task ReloadResult()
{
// BlReloadEnabled = true;
ShowClashUI = showClashUI;
TabMainSelectedIndex = showClashUI ? TabMainSelectedIndex : 0;
//Locator.Current.GetService<StatusBarViewModel>()?.ChangeSystemProxyAsync(_config.systemProxyItem.sysProxyType, false);
ShowClashUI = _config.IsRunningCore(ECoreType.sing_box);
if (ShowClashUI)
{
Locator.Current.GetService<ClashProxiesViewModel>()?.ProxiesReload();
}
else
{
TabMainSelectedIndex = 0;
}
}
private async Task LoadCore()
@ -576,11 +566,17 @@ public class MainWindowViewModel : MyReactiveObject
await CoreManager.Instance.LoadCore(node);
}
public async Task CloseCore()
{
await ConfigHandler.SaveConfig(_config);
await CoreManager.Instance.CoreStop();
}
private async Task AutoHideStartup()
{
if (_config.UiItem.AutoHideStartup)
{
AppEvents.ShowHideWindowRequested.Publish(false);
ShowHideWindow(false);
}
await Task.CompletedTask;
}
@ -593,7 +589,7 @@ public class MainWindowViewModel : MyReactiveObject
{
await ConfigHandler.ApplyRegionalPreset(_config, type);
await ConfigHandler.InitRouting(_config);
AppEvents.RoutingsMenuRefreshRequested.Publish();
Locator.Current.GetService<StatusBarViewModel>()?.RefreshRoutingsMenu();
await ConfigHandler.SaveConfig(_config);
await new UpdateService().UpdateGeoFileAll(_config, UpdateTaskHandler);

View file

@ -1,6 +1,5 @@
using System.Collections.Concurrent;
using System.Reactive.Linq;
using System.Text;
using System.Text.RegularExpressions;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
@ -10,9 +9,9 @@ namespace ServiceLib.ViewModels;
public class MsgViewModel : MyReactiveObject
{
private readonly ConcurrentQueue<string> _queueMsg = new();
private volatile bool _lastMsgFilterNotAvailable;
private int _showLock = 0; // 0 = unlocked, 1 = locked
public int NumMaxMsg { get; } = 500;
private readonly int _numMaxMsg = 500;
private bool _lastMsgFilterNotAvailable;
private bool _blLockShow = false;
[Reactive]
public string MsgFilter { get; set; }
@ -34,52 +33,46 @@ public class MsgViewModel : MyReactiveObject
this.WhenAnyValue(
x => x.AutoRefresh,
y => y == true)
.Subscribe(c => _config.MsgUIItem.AutoRefresh = AutoRefresh);
.Subscribe(c => { _config.MsgUIItem.AutoRefresh = AutoRefresh; });
AppEvents.SendMsgViewRequested
.AsObservable()
//.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => _ = AppendQueueMsg(content));
.Subscribe(async content => await AppendQueueMsg(content));
}
private async Task AppendQueueMsg(string msg)
{
//if (msg == Global.CommandClearMsg)
//{
// ClearMsg();
// return;
//}
if (AutoRefresh == false)
{
return;
}
_ = EnqueueQueueMsg(msg);
EnqueueQueueMsg(msg);
if (_blLockShow)
{
return;
}
if (!_config.UiItem.ShowInTaskbar)
{
return;
}
if (Interlocked.CompareExchange(ref _showLock, 1, 0) != 0)
{
return;
}
_blLockShow = true;
try
{
await Task.Delay(500).ConfigureAwait(false);
await Task.Delay(500);
var txt = string.Join("", _queueMsg.ToArray());
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, txt);
var sb = new StringBuilder();
while (_queueMsg.TryDequeue(out var line))
{
sb.Append(line);
}
await _updateView?.Invoke(EViewAction.DispatcherShowMsg, sb.ToString());
}
finally
{
Interlocked.Exchange(ref _showLock, 0);
}
_blLockShow = false;
}
private void EnqueueQueueMsg(string msg)
private async Task EnqueueQueueMsg(string msg)
{
//filter msg
if (MsgFilter.IsNotEmpty() && !_lastMsgFilterNotAvailable)
@ -98,17 +91,26 @@ public class MsgViewModel : MyReactiveObject
}
}
//Enqueue
if (_queueMsg.Count > _numMaxMsg)
{
for (int k = 0; k < _queueMsg.Count - _numMaxMsg; k++)
{
_queueMsg.TryDequeue(out _);
}
}
_queueMsg.Enqueue(msg);
if (!msg.EndsWith(Environment.NewLine))
{
_queueMsg.Enqueue(Environment.NewLine);
}
await Task.CompletedTask;
}
//public void ClearMsg()
//{
// _queueMsg.Clear();
//}
public void ClearMsg()
{
_queueMsg.Clear();
}
private void DoMsgFilter()
{

View file

@ -6,6 +6,7 @@ using DynamicData;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
@ -249,21 +250,11 @@ public class ProfilesViewModel : MyReactiveObject
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshServersBiz());
AppEvents.SubscriptionsRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshSubscriptions());
AppEvents.DispatcherStatisticsRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
AppEvents.SetDefaultServerRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async indexId => await SetDefaultServer(indexId));
#endregion AppEvents
_ = Init();
@ -285,7 +276,7 @@ public class ProfilesViewModel : MyReactiveObject
private void Reload()
{
AppEvents.ReloadRequested.Publish();
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
}
public async Task SetSpeedTestResult(SpeedTestResult result)
@ -371,7 +362,7 @@ public class ProfilesViewModel : MyReactiveObject
public async Task RefreshServers()
{
AppEvents.ProfilesRefreshRequested.Publish();
AppEvents.ProfilesRefreshRequested.OnNext(Unit.Default);
await Task.Delay(200);
}
@ -399,7 +390,7 @@ public class ProfilesViewModel : MyReactiveObject
await _updateView?.Invoke(EViewAction.DispatcherRefreshServersBiz, null);
}
private async Task RefreshSubscriptions()
public async Task RefreshSubscriptions()
{
SubItems.Clear();
@ -588,7 +579,7 @@ public class ProfilesViewModel : MyReactiveObject
await SetDefaultServer(SelectedProfile.IndexId);
}
private async Task SetDefaultServer(string? indexId)
public async Task SetDefaultServer(string? indexId)
{
if (indexId.IsNullOrEmpty())
{

View file

@ -5,14 +5,12 @@ using System.Text;
using DynamicData.Binding;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Splat;
namespace ServiceLib.ViewModels;
public class StatusBarViewModel : MyReactiveObject
{
private static readonly Lazy<StatusBarViewModel> _instance = new(() => new(null));
public static StatusBarViewModel Instance => _instance.Value;
#region ObservableCollection
public IObservableCollection<RoutingItem> RoutingItems { get; } = new ObservableCollectionExtended<RoutingItem>();
@ -148,17 +146,17 @@ public class StatusBarViewModel : MyReactiveObject
NotifyLeftClickCmd = ReactiveCommand.CreateFromTask(async () =>
{
AppEvents.ShowHideWindowRequested.Publish(null);
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(null);
await Task.CompletedTask;
});
ShowWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{
AppEvents.ShowHideWindowRequested.Publish(true);
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(true);
await Task.CompletedTask;
});
HideWindowCmd = ReactiveCommand.CreateFromTask(async () =>
{
AppEvents.ShowHideWindowRequested.Publish(false);
Locator.Current.GetService<MainWindowViewModel>()?.ShowHideWindow(false);
await Task.CompletedTask;
});
@ -211,26 +209,6 @@ public class StatusBarViewModel : MyReactiveObject
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await UpdateStatistics(result));
AppEvents.RoutingsMenuRefreshRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await RefreshRoutingsMenu());
AppEvents.TestServerRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await TestServerAvailability());
AppEvents.InboundDisplayRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async _ => await InboundDisplayStatus());
AppEvents.SysProxyChangeRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(async result => await SetListenerType(result));
#endregion AppEvents
_ = Init();
@ -238,7 +216,6 @@ public class StatusBarViewModel : MyReactiveObject
private async Task Init()
{
await ConfigHandler.InitBuiltinRouting(_config);
await RefreshRoutingsMenu();
await InboundDisplayStatus();
await ChangeSystemProxyAsync(_config.SystemProxyItem.SysProxyType, true);
@ -275,20 +252,23 @@ public class StatusBarViewModel : MyReactiveObject
private async Task AddServerViaClipboard()
{
AppEvents.AddServerViaClipboardRequested.Publish();
await Task.Delay(1000);
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
await service.AddServerViaClipboardAsync(null);
}
private async Task AddServerViaScan()
{
AppEvents.AddServerViaScanRequested.Publish();
await Task.Delay(1000);
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
await service.AddServerViaScanAsync();
}
private async Task UpdateSubscriptionProcess(bool blProxy)
{
AppEvents.SubscriptionsUpdateRequested.Publish(blProxy);
await Task.Delay(1000);
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
await service.UpdateSubscriptionProcess("", blProxy);
}
private async Task RefreshServersBiz()
@ -349,7 +329,7 @@ public class StatusBarViewModel : MyReactiveObject
{
return;
}
AppEvents.SetDefaultServerRequested.Publish(SelectedServer.ID);
Locator.Current.GetService<ProfilesViewModel>()?.SetDefaultServer(SelectedServer.ID);
}
public async Task TestServerAvailability()
@ -384,7 +364,7 @@ public class StatusBarViewModel : MyReactiveObject
#region System proxy and Routings
private async Task SetListenerType(ESysProxyType type)
public async Task SetListenerType(ESysProxyType type)
{
if (_config.SystemProxyItem.SysProxyType == type)
{
@ -413,7 +393,7 @@ public class StatusBarViewModel : MyReactiveObject
}
}
private async Task RefreshRoutingsMenu()
public async Task RefreshRoutingsMenu()
{
RoutingItems.Clear();
@ -450,7 +430,7 @@ public class StatusBarViewModel : MyReactiveObject
if (await ConfigHandler.SetDefaultRouting(_config, item) == 0)
{
NoticeManager.Instance.SendMessageEx(ResUI.TipChangeRouting);
AppEvents.ReloadRequested.Publish();
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
_updateView?.Invoke(EViewAction.DispatcherRefreshIcon, null);
}
}
@ -483,7 +463,7 @@ public class StatusBarViewModel : MyReactiveObject
if (Utils.IsWindows())
{
_config.TunModeItem.EnableTun = false;
await AppManager.Instance.RebootAsAdmin();
Locator.Current.GetService<MainWindowViewModel>()?.RebootAsAdmin();
return;
}
else
@ -497,7 +477,7 @@ public class StatusBarViewModel : MyReactiveObject
}
}
await ConfigHandler.SaveConfig(_config);
AppEvents.ReloadRequested.Publish();
Locator.Current.GetService<MainWindowViewModel>()?.Reload();
}
private bool AllowEnableTun()
@ -521,7 +501,7 @@ public class StatusBarViewModel : MyReactiveObject
#region UI
private async Task InboundDisplayStatus()
public async Task InboundDisplayStatus()
{
StringBuilder sb = new();
sb.Append($"[{EInboundProtocol.mixed}:{AppManager.Instance.GetLocalPort(EInboundProtocol.socks)}");

View file

@ -11,7 +11,6 @@
RequestedThemeVariant="Default">
<Application.Styles>
<semi:SemiTheme />
<semi:AvaloniaEditSemiTheme />
<StyleInclude Source="Assets/GlobalStyles.axaml" />
<StyleInclude Source="avares://Semi.Avalonia.DataGrid/Index.axaml" />
<dialogHost:DialogHostStyles />

View file

@ -1,6 +1,8 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Splat;
using v2rayN.Desktop.Common;
using v2rayN.Desktop.Views;
namespace v2rayN.Desktop;
@ -14,7 +16,9 @@ public partial class App : Application
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
TaskScheduler.UnobservedTaskException += TaskScheduler_UnobservedTaskException;
DataContext = StatusBarViewModel.Instance;
var ViewModel = new StatusBarViewModel(null);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(StatusBarViewModel));
DataContext = ViewModel;
}
public override void OnFrameworkInitializationCompleted()
@ -53,8 +57,16 @@ public partial class App : Application
{
if (desktop.MainWindow != null)
{
AppEvents.AddServerViaClipboardRequested.Publish();
await Task.Delay(1000);
var clipboardData = await AvaUtils.GetClipboardData(desktop.MainWindow);
if (clipboardData.IsNullOrEmpty())
{
return;
}
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
{
_ = service.AddServerViaClipboardAsync(clipboardData);
}
}
}
}

View file

@ -1,129 +0,0 @@
using Avalonia.Media;
using AvaloniaEdit;
using AvaloniaEdit.Document;
using AvaloniaEdit.Rendering;
namespace v2rayN.Desktop.Common;
public class KeywordColorizer : DocumentColorizingTransformer
{
private readonly string[] _keywords;
private readonly Dictionary<string, IBrush> _brushMap;
public KeywordColorizer(IDictionary<string, IBrush> keywordBrushMap)
{
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
{
throw new ArgumentException("keywordBrushMap must not be null or empty", nameof(keywordBrushMap));
}
_brushMap = new Dictionary<string, IBrush>(StringComparer.OrdinalIgnoreCase);
foreach (var kvp in keywordBrushMap)
{
if (string.IsNullOrEmpty(kvp.Key) || kvp.Value == null)
{
continue;
}
if (!_brushMap.ContainsKey(kvp.Key))
{
_brushMap[kvp.Key] = kvp.Value;
}
}
if (_brushMap.Count == 0)
{
throw new ArgumentException("keywordBrushMap must contain at least one non-empty key with a non-null brush", nameof(keywordBrushMap));
}
_keywords = _brushMap.Keys.ToArray();
}
protected override void ColorizeLine(DocumentLine line)
{
var text = CurrentContext.Document.GetText(line);
if (string.IsNullOrEmpty(text))
{
return;
}
foreach (var kw in _keywords)
{
if (string.IsNullOrEmpty(kw))
{
continue;
}
var searchStart = 0;
while (true)
{
var idx = text.IndexOf(kw, searchStart, StringComparison.OrdinalIgnoreCase);
if (idx < 0)
{
break;
}
var kwEndIndex = idx + kw.Length;
if (IsWordCharBefore(text, idx) || IsWordCharAfter(text, kwEndIndex))
{
searchStart = idx + Math.Max(1, kw.Length);
continue;
}
var start = line.Offset + idx;
var end = start + kw.Length;
if (_brushMap.TryGetValue(kw, out var brush) && brush != null)
{
ChangeLinePart(start, end, element => element.TextRunProperties.SetForegroundBrush(brush));
}
searchStart = idx + Math.Max(1, kw.Length);
}
}
}
private static bool IsWordCharBefore(string text, int idx)
{
if (idx <= 0)
{
return false;
}
var c = text[idx - 1];
return char.IsLetterOrDigit(c) || c == '_';
}
private static bool IsWordCharAfter(string text, int idx)
{
if (idx >= text.Length)
{
return false;
}
var c = text[idx];
return char.IsLetterOrDigit(c) || c == '_';
}
}
public static class TextEditorKeywordHighlighter
{
public static void Attach(TextEditor editor, IDictionary<string, IBrush> keywordBrushMap)
{
ArgumentNullException.ThrowIfNull(editor);
if (keywordBrushMap == null || keywordBrushMap.Count == 0)
{
return;
}
if (editor.TextArea?.TextView?.LineTransformers?.OfType<KeywordColorizer>().Any() == true)
{
return;
}
var colorizer = new KeywordColorizer(keywordBrushMap);
editor.TextArea.TextView.LineTransformers.Add(colorizer);
editor.TextArea.TextView.InvalidateVisual();
}
}

View file

@ -2,7 +2,6 @@ global using ServiceLib;
global using ServiceLib.Base;
global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Manager;
global using ServiceLib.Models;

View file

@ -5,7 +5,6 @@ using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Media;
using Avalonia.Styling;
using AvaloniaEdit;
using ReactiveUI;
using ReactiveUI.Fody.Helpers;
using Semi.Avalonia;
@ -113,8 +112,7 @@ public class ThemeSettingViewModel : MyReactiveObject
x.OfType<ContextMenu>(),
x.OfType<DataGridRow>(),
x.OfType<ListBoxItem>(),
x.OfType<HeaderedContentControl>(),
x.OfType<TextEditor>()
x.OfType<HeaderedContentControl>()
));
style.Add(new Setter()
{
@ -155,8 +153,7 @@ public class ThemeSettingViewModel : MyReactiveObject
x.OfType<DataGridRow>(),
x.OfType<ListBoxItem>(),
x.OfType<HeaderedContentControl>(),
x.OfType<WindowNotificationManager>(),
x.OfType<TextEditor>()
x.OfType<WindowNotificationManager>()
));
style.Add(new Setter()
{

View file

@ -3,6 +3,7 @@ using Avalonia.Input;
using Avalonia.ReactiveUI;
using DynamicData;
using ReactiveUI;
using Splat;
namespace v2rayN.Desktop.Views;
@ -12,6 +13,7 @@ public partial class ClashProxiesView : ReactiveUserControl<ClashProxiesViewMode
{
InitializeComponent();
ViewModel = new ClashProxiesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ClashProxiesViewModel));
lstProxyDetails.DoubleTapped += LstProxyDetails_DoubleTapped;
this.KeyDown += ClashProxiesView_KeyDown;

View file

@ -10,6 +10,7 @@ using Avalonia.Threading;
using DialogHostAvalonia;
using MsBox.Avalonia.Enums;
using ReactiveUI;
using Splat;
using v2rayN.Desktop.Base;
using v2rayN.Desktop.Common;
using v2rayN.Desktop.Manager;
@ -19,7 +20,7 @@ namespace v2rayN.Desktop.Views;
public partial class MainWindow : WindowBase<MainWindowViewModel>
{
private static Config _config;
private readonly WindowNotificationManager? _manager;
private WindowNotificationManager? _manager;
private CheckUpdateView? _checkUpdateView;
private BackupAndRestoreView? _backupAndRestoreView;
private bool _blCloseByUser = false;
@ -39,6 +40,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
menuClose.Click += MenuClose_Click;
ViewModel = new MainWindowViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(MainWindowViewModel));
switch (_config.UiItem.MainGirdOrientation)
{
@ -153,12 +155,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => Shutdown(content))
.DisposeWith(disposables);
AppEvents.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(blShow => ShowHideWindow(blShow))
.DisposeWith(disposables);
});
if (Utils.IsWindows())
@ -232,6 +228,12 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
case EViewAction.SubSettingWindow:
return await new SubSettingWindow().ShowDialog<bool>(this);
case EViewAction.ShowHideWindow:
Dispatcher.UIThread.Post(() =>
ShowHideWindow((bool?)obj),
DispatcherPriority.Default);
break;
case EViewAction.ScanScreenTask:
await ScanScreenTaskAsync();
break;
@ -241,7 +243,11 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
break;
case EViewAction.AddServerViaClipboard:
await AddServerViaClipboardAsync();
var clipboardData = await AvaUtils.GetClipboardData(this);
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
break;
}
@ -260,7 +266,7 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
case EGlobalHotkey.SystemProxySet:
case EGlobalHotkey.SystemProxyUnchanged:
case EGlobalHotkey.SystemProxyPac:
AppEvents.SysProxyChangeRequested.Publish((ESysProxyType)((int)e - 1));
Locator.Current.GetService<StatusBarViewModel>()?.SetListenerType((ESysProxyType)((int)e - 1));
break;
}
}
@ -296,7 +302,11 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
switch (e.Key)
{
case Key.V:
await AddServerViaClipboardAsync();
var clipboardData = await AvaUtils.GetClipboardData(this);
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
break;
case Key.S:
@ -323,15 +333,6 @@ public partial class MainWindow : WindowBase<MainWindowViewModel>
ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
}
public async Task AddServerViaClipboardAsync()
{
var clipboardData = await AvaUtils.GetClipboardData(this);
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
}
public async Task ScanScreenTaskAsync()
{
//ShowHideWindow(false);

View file

@ -2,7 +2,6 @@
x:Class="v2rayN.Desktop.Views.MsgView"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avaloniaEdit="clr-namespace:AvaloniaEdit;assembly=AvaloniaEdit"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:resx="clr-namespace:ServiceLib.Resx;assembly=ServiceLib"
@ -71,36 +70,35 @@
Theme="{DynamicResource SimpleToggleSwitch}" />
</WrapPanel>
<avaloniaEdit:TextEditor
Name="txtMsg"
Margin="{StaticResource Margin8}"
IsReadOnly="True"
VerticalScrollBarVisibility="Auto"
WordWrap="True">
<avaloniaEdit:TextEditor.Options>
<avaloniaEdit:TextEditorOptions AllowScrollBelowDocument="False"/>
</avaloniaEdit:TextEditor.Options>
<avaloniaEdit:TextEditor.ContextFlyout>
<MenuFlyout>
<MenuItem
x:Name="menuMsgViewSelectAll"
Click="menuMsgViewSelectAll_Click"
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
<MenuItem
x:Name="menuMsgViewCopy"
Click="menuMsgViewCopy_Click"
Header="{x:Static resx:ResUI.menuMsgViewCopy}" />
<MenuItem
x:Name="menuMsgViewCopyAll"
Click="menuMsgViewCopyAll_Click"
Header="{x:Static resx:ResUI.menuMsgViewCopyAll}" />
<MenuItem
x:Name="menuMsgViewClear"
Click="menuMsgViewClear_Click"
Header="{x:Static resx:ResUI.menuMsgViewClear}" />
</MenuFlyout>
</avaloniaEdit:TextEditor.ContextFlyout>
</avaloniaEdit:TextEditor>
<ScrollViewer x:Name="msgScrollViewer" VerticalScrollBarVisibility="Auto">
<SelectableTextBlock
Name="txtMsg"
Margin="{StaticResource Margin8}"
VerticalAlignment="Stretch"
Classes="TextArea"
TextAlignment="Left"
TextWrapping="Wrap">
<SelectableTextBlock.ContextMenu>
<ContextMenu>
<MenuItem
x:Name="menuMsgViewSelectAll"
Click="menuMsgViewSelectAll_Click"
Header="{x:Static resx:ResUI.menuMsgViewSelectAll}" />
<MenuItem
x:Name="menuMsgViewCopy"
Click="menuMsgViewCopy_Click"
Header="{x:Static resx:ResUI.menuMsgViewCopy}" />
<MenuItem
x:Name="menuMsgViewCopyAll"
Click="menuMsgViewCopyAll_Click"
Header="{x:Static resx:ResUI.menuMsgViewCopyAll}" />
<MenuItem
x:Name="menuMsgViewClear"
Click="menuMsgViewClear_Click"
Header="{x:Static resx:ResUI.menuMsgViewClear}" />
</ContextMenu>
</SelectableTextBlock.ContextMenu>
</SelectableTextBlock>
</ScrollViewer>
</DockPanel>
</UserControl>

View file

@ -1,6 +1,6 @@
using System.Reactive.Disposables;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
@ -10,12 +10,13 @@ namespace v2rayN.Desktop.Views;
public partial class MsgView : ReactiveUserControl<MsgViewModel>
{
//private const int KeepLines = 30;
private readonly ScrollViewer _scrollViewer;
public MsgView()
{
InitializeComponent();
txtMsg.TextArea.TextView.Options.EnableHyperlinks = false;
_scrollViewer = this.FindControl<ScrollViewer>("msgScrollViewer");
ViewModel = new MsgViewModel(UpdateViewHandler);
this.WhenActivated(disposables =>
@ -23,11 +24,6 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
this.Bind(ViewModel, vm => vm.MsgFilter, v => v.cmbMsgFilter.Text).DisposeWith(disposables);
this.Bind(ViewModel, vm => vm.AutoRefresh, v => v.togAutoRefresh.IsChecked).DisposeWith(disposables);
});
TextEditorKeywordHighlighter.Attach(txtMsg, Global.LogLevelColors.ToDictionary(
kv => kv.Key,
kv => (IBrush)new SolidColorBrush(Color.Parse(kv.Value))
));
}
private async Task<bool> UpdateViewHandler(EViewAction action, object? obj)
@ -38,8 +34,9 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
if (obj is null)
return false;
Dispatcher.UIThread.Post(() => ShowMsg(obj),
DispatcherPriority.ApplicationIdle);
Dispatcher.UIThread.Post(() =>
ShowMsg(obj),
DispatcherPriority.ApplicationIdle);
break;
}
return await Task.FromResult(true);
@ -47,37 +44,23 @@ public partial class MsgView : ReactiveUserControl<MsgViewModel>
private void ShowMsg(object msg)
{
//var lineCount = txtMsg.LineCount;
//if (lineCount > ViewModel?.NumMaxMsg)
//{
// var cutLine = txtMsg.Document.GetLineByNumber(lineCount - KeepLines);
// txtMsg.Document.Remove(0, cutLine.Offset);
//}
if (txtMsg.LineCount > ViewModel?.NumMaxMsg)
{
ClearMsg();
}
txtMsg.AppendText(msg.ToString());
txtMsg.Text = msg.ToString();
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollToEnd();
_scrollViewer?.ScrollToEnd();
}
}
public void ClearMsg()
{
txtMsg.Clear();
txtMsg.AppendText("----- Message cleared -----\n");
ViewModel?.ClearMsg();
txtMsg.Text = "";
}
private void menuMsgViewSelectAll_Click(object? sender, RoutedEventArgs e)
{
Dispatcher.UIThread.Post(() =>
{
txtMsg.TextArea.Focus();
txtMsg.SelectAll();
}, DispatcherPriority.Render);
txtMsg.Focus();
txtMsg.SelectAll();
}
private async void menuMsgViewCopy_Click(object? sender, RoutedEventArgs e)

View file

@ -3,13 +3,13 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.ReactiveUI;
using Avalonia.VisualTree;
using ReactiveUI;
using v2rayN.Desktop.Base;
namespace v2rayN.Desktop.Views;
public partial class ProfilesSelectWindow : WindowBase<ProfilesSelectViewModel>
public partial class ProfilesSelectWindow : ReactiveWindow<ProfilesSelectViewModel>
{
private static Config _config;

View file

@ -8,6 +8,7 @@ using Avalonia.Threading;
using DialogHostAvalonia;
using MsBox.Avalonia.Enums;
using ReactiveUI;
using Splat;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views;
@ -47,6 +48,7 @@ public partial class ProfilesView : ReactiveUserControl<ProfilesViewModel>
//}
ViewModel = new ProfilesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ProfilesViewModel));
this.WhenActivated(disposables =>
{

View file

@ -10,7 +10,7 @@
mc:Ignorable="d">
<UserControl.Resources>
<sys:Double x:Key="QrcodeWidth">400</sys:Double>
<sys:Double x:Key="QrcodeWidth">500</sys:Double>
</UserControl.Resources>
<Grid Margin="32" RowDefinitions="Auto,Auto">

View file

@ -6,6 +6,7 @@ using Avalonia.ReactiveUI;
using Avalonia.Threading;
using DialogHostAvalonia;
using ReactiveUI;
using Splat;
using v2rayN.Desktop.Common;
namespace v2rayN.Desktop.Views;
@ -19,8 +20,9 @@ public partial class StatusBarView : ReactiveUserControl<StatusBarViewModel>
InitializeComponent();
_config = AppManager.Instance.Config;
ViewModel = StatusBarViewModel.Instance;
//ViewModel = new StatusBarViewModel(UpdateViewHandler);
//Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(StatusBarViewModel));
ViewModel = Locator.Current.GetService<StatusBarViewModel>();
ViewModel?.InitUpdateView(UpdateViewHandler);
txtRunningServerDisplay.Tapped += TxtRunningServerDisplay_Tapped;

View file

@ -9,7 +9,6 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia.AvaloniaEdit" />
<PackageReference Include="Avalonia.Controls.DataGrid">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>
@ -18,7 +17,6 @@
<PackageReference Include="Avalonia.ReactiveUI" />
<PackageReference Include="MessageBox.Avalonia" />
<PackageReference Include="Semi.Avalonia" />
<PackageReference Include="Semi.Avalonia.AvaloniaEdit" />
<PackageReference Include="Semi.Avalonia.DataGrid">
<TreatAsUsed>true</TreatAsUsed>
</PackageReference>

View file

@ -2,7 +2,6 @@ global using ServiceLib;
global using ServiceLib.Base;
global using ServiceLib.Common;
global using ServiceLib.Enums;
global using ServiceLib.Events;
global using ServiceLib.Handler;
global using ServiceLib.Manager;
global using ServiceLib.Models;

View file

@ -1,6 +1,7 @@
using System.Reactive.Disposables;
using System.Windows.Input;
using ReactiveUI;
using Splat;
namespace v2rayN.Views;
@ -13,6 +14,7 @@ public partial class ClashProxiesView
{
InitializeComponent();
ViewModel = new ClashProxiesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ClashProxiesViewModel));
lstProxyDetails.PreviewMouseDoubleClick += lstProxyDetails_PreviewMouseDoubleClick;
this.WhenActivated(disposables =>

View file

@ -6,8 +6,10 @@ using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interop;
using System.Windows.Media;
using System.Windows.Threading;
using MaterialDesignThemes.Wpf;
using ReactiveUI;
using Splat;
using v2rayN.Manager;
namespace v2rayN.Views;
@ -35,6 +37,7 @@ public partial class MainWindow
menuBackupAndRestore.Click += MenuBackupAndRestore_Click;
ViewModel = new MainWindowViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(MainWindowViewModel));
switch (_config.UiItem.MainGirdOrientation)
{
@ -145,16 +148,10 @@ public partial class MainWindow
.DisposeWith(disposables);
AppEvents.ShutdownRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => Shutdown(content))
.DisposeWith(disposables);
AppEvents.ShowHideWindowRequested
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(blShow => ShowHideWindow(blShow))
.DisposeWith(disposables);
.AsObservable()
.ObserveOn(RxApp.MainThreadScheduler)
.Subscribe(content => Shutdown(content))
.DisposeWith(disposables);
});
this.Title = $"{Utils.GetVersion()} - {(Utils.IsAdministrator() ? ResUI.RunAsAdmin : ResUI.NotRunAsAdmin)}";
@ -220,6 +217,13 @@ public partial class MainWindow
case EViewAction.SubSettingWindow:
return (new SubSettingWindow().ShowDialog() ?? false);
case EViewAction.ShowHideWindow:
Application.Current?.Dispatcher.Invoke((() =>
{
ShowHideWindow((bool?)obj);
}), DispatcherPriority.Normal);
break;
case EViewAction.ScanScreenTask:
await ScanScreenTaskAsync();
break;
@ -229,7 +233,11 @@ public partial class MainWindow
break;
case EViewAction.AddServerViaClipboard:
await AddServerViaClipboardAsync();
var clipboardData = WindowsUtils.GetClipboardData();
if (clipboardData.IsNotEmpty())
{
ViewModel?.AddServerViaClipboardAsync(clipboardData);
}
break;
}
@ -248,7 +256,7 @@ public partial class MainWindow
case EGlobalHotkey.SystemProxySet:
case EGlobalHotkey.SystemProxyUnchanged:
case EGlobalHotkey.SystemProxyPac:
AppEvents.SysProxyChangeRequested.Publish((ESysProxyType)((int)e - 1));
Locator.Current.GetService<StatusBarViewModel>()?.SetListenerType((ESysProxyType)((int)e - 1));
break;
}
}
@ -282,7 +290,16 @@ public partial class MainWindow
{
return;
}
AddServerViaClipboardAsync().ContinueWith(_ => { });
var clipboardData = WindowsUtils.GetClipboardData();
if (clipboardData.IsNotEmpty())
{
var service = Locator.Current.GetService<MainWindowViewModel>();
if (service != null)
{
_ = service.AddServerViaClipboardAsync(clipboardData);
}
}
break;
@ -316,15 +333,6 @@ public partial class MainWindow
ProcUtils.ProcessStart(Utils.GetBinPath("EnableLoopback.exe"));
}
public async Task AddServerViaClipboardAsync()
{
var clipboardData = WindowsUtils.GetClipboardData();
if (clipboardData.IsNotEmpty() && ViewModel != null)
{
await ViewModel.AddServerViaClipboardAsync(clipboardData);
}
}
private async Task ScanScreenTaskAsync()
{
ShowHideWindow(false);

View file

@ -47,22 +47,19 @@ public partial class MsgView
private void ShowMsg(object msg)
{
if (txtMsg.LineCount > ViewModel?.NumMaxMsg)
{
ClearMsg();
}
txtMsg.AppendText(msg.ToString());
txtMsg.BeginChange();
txtMsg.Text = msg.ToString();
if (togScrollToEnd.IsChecked ?? true)
{
txtMsg.ScrollToEnd();
}
txtMsg.EndChange();
}
public void ClearMsg()
{
ViewModel?.ClearMsg();
txtMsg.Clear();
txtMsg.AppendText("----- Message cleared -----\n");
}
private void menuMsgViewSelectAll_Click(object sender, System.Windows.RoutedEventArgs e)

View file

@ -8,6 +8,7 @@ using System.Windows.Media;
using System.Windows.Threading;
using MaterialDesignThemes.Wpf;
using ReactiveUI;
using Splat;
using v2rayN.Base;
using Point = System.Windows.Point;
@ -41,6 +42,7 @@ public partial class ProfilesView
}
ViewModel = new ProfilesViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(ProfilesViewModel));
this.WhenActivated(disposables =>
{

View file

@ -12,7 +12,7 @@
Style="{StaticResource ViewGlobal}"
mc:Ignorable="d">
<UserControl.Resources>
<sys:Double x:Key="QrcodeWidth">400</sys:Double>
<sys:Double x:Key="QrcodeWidth">500</sys:Double>
</UserControl.Resources>
<Grid Margin="32">

View file

@ -3,6 +3,7 @@ using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using ReactiveUI;
using Splat;
using v2rayN.Manager;
namespace v2rayN.Views;
@ -15,8 +16,8 @@ public partial class StatusBarView
{
InitializeComponent();
_config = AppManager.Instance.Config;
ViewModel = StatusBarViewModel.Instance;
ViewModel?.InitUpdateView(UpdateViewHandler);
ViewModel = new StatusBarViewModel(UpdateViewHandler);
Locator.CurrentMutable.RegisterLazySingleton(() => ViewModel, typeof(StatusBarViewModel));
menuExit.Click += menuExit_Click;
txtRunningServerDisplay.PreviewMouseDown += txtRunningInfoDisplay_MouseDoubleClick;