namespace ServiceLib.ViewModels; public partial class ProfilesViewModel : MyReactiveObject { public Interaction ShowYesNoInteraction { get; } = new(); public Interaction SaveFileDialogInteraction { get; } = new(); public Interaction SetClipboardDataInteraction { get; } = new(); public Interaction ProfilesFocusInteraction { get; } = new(); public Interaction ShareServerInteraction { get; } = new(); public Interaction DispatcherRefreshServersBizInteraction { get; } = new(); public Interaction AdjustMainLvColWidthInteraction { get; } = new(); public EventChannel ReloadRequested { get; } = new(); public EventChannel RefreshServersRequested { get; } = new(); #region private prop private List _lstProfile; private string _serverFilter = string.Empty; private readonly Dictionary _dicHeaderSort = new(); private SpeedtestService? _speedtestService; private string? _pendingSelectIndexId; #endregion private prop #region ObservableCollection public BulkObservableCollection ProfileItems { get; } = []; public BulkObservableCollection SubItems { get; } = []; [Reactive] public partial ProfileItemModel SelectedProfile { get; set; } public IList SelectedProfiles { get; set; } [Reactive] public partial SubItem SelectedSub { get; set; } [Reactive] public partial SubItem SelectedMoveToGroup { get; set; } [Reactive] public partial string ServerFilter { get; set; } #endregion ObservableCollection #region Menu //servers delete public ReactiveCommand EditServerCmd { get; } public ReactiveCommand RemoveServerCmd { get; } public ReactiveCommand RemoveDuplicateServerCmd { get; } public ReactiveCommand CopyServerCmd { get; } public ReactiveCommand SetDefaultServerCmd { get; } public ReactiveCommand ShareServerCmd { get; } public ReactiveCommand GenGroupAllServerCmd { get; } public ReactiveCommand GenGroupRegionServerCmd { get; } //servers move public ReactiveCommand MoveTopCmd { get; } public ReactiveCommand MoveUpCmd { get; } public ReactiveCommand MoveDownCmd { get; } public ReactiveCommand MoveBottomCmd { get; } public ReactiveCommand MoveToGroupCmd { get; } //servers ping public ReactiveCommand MixedTestServerCmd { get; } public ReactiveCommand TcpingServerCmd { get; } public ReactiveCommand RealPingServerCmd { get; } public ReactiveCommand UdpTestServerCmd { get; } public ReactiveCommand SpeedServerCmd { get; } public ReactiveCommand SortServerResultCmd { get; } public ReactiveCommand RemoveInvalidServerResultCmd { get; } public ReactiveCommand FastRealPingCmd { get; } //servers export public ReactiveCommand Export2ClientConfigCmd { get; } public ReactiveCommand Export2ClientConfigClipboardCmd { get; } public ReactiveCommand Export2ShareUrlCmd { get; } public ReactiveCommand Export2ShareUrlBase64Cmd { get; } public ReactiveCommand Export2InnerUriCmd { get; } public ReactiveCommand AddSubCmd { get; } public ReactiveCommand EditSubCmd { get; } public ReactiveCommand DeleteSubCmd { get; } #endregion Menu #region Init public ProfilesViewModel() { _config = AppManager.Instance.Config; #region WhenAnyValue && ReactiveCommand var canEditRemove = this.WhenAnyValue( x => x.SelectedProfile, selectedSource => selectedSource != null && !selectedSource.IndexId.IsNullOrEmpty()); this.WhenAnyValue( x => x.SelectedSub, y => y != null && !y.Remarks.IsNullOrEmpty() && _config.SubIndexId != y.Id) .Subscribe(async c => await SubSelectedChangedAsync(c)); this.WhenAnyValue( x => x.SelectedMoveToGroup, y => y != null && !y.Remarks.IsNullOrEmpty()) .Subscribe(async c => await MoveToGroup(c)); this.WhenAnyValue( x => x.ServerFilter, y => y != null && _serverFilter != y) .Subscribe(async c => await ServerFilterChanged(c)); //servers delete EditServerCmd = ReactiveCommand.CreateFromTask(async () => { await EditServerAsync(); }, canEditRemove); RemoveServerCmd = ReactiveCommand.CreateFromTask(async () => { await RemoveServerAsync(); }, canEditRemove); RemoveDuplicateServerCmd = ReactiveCommand.CreateFromTask(async () => { await RemoveDuplicateServer(); }); CopyServerCmd = ReactiveCommand.CreateFromTask(async () => { await CopyServer(); }, canEditRemove); SetDefaultServerCmd = ReactiveCommand.CreateFromTask(async () => { await SetDefaultServer(); }, canEditRemove); ShareServerCmd = ReactiveCommand.CreateFromTask(async () => { await ShareServerAsync(); }, canEditRemove); GenGroupAllServerCmd = ReactiveCommand.CreateFromTask(async () => { await GenGroupAllServer(); }, canEditRemove); GenGroupRegionServerCmd = ReactiveCommand.CreateFromTask(async () => { await GenGroupRegionServer(); }, canEditRemove); //servers move MoveTopCmd = ReactiveCommand.CreateFromTask(async () => { await MoveServer(EMove.Top); }, canEditRemove); MoveUpCmd = ReactiveCommand.CreateFromTask(async () => { await MoveServer(EMove.Up); }, canEditRemove); MoveDownCmd = ReactiveCommand.CreateFromTask(async () => { await MoveServer(EMove.Down); }, canEditRemove); MoveBottomCmd = ReactiveCommand.CreateFromTask(async () => { await MoveServer(EMove.Bottom); }, canEditRemove); MoveToGroupCmd = ReactiveCommand.CreateFromTask(async sub => { SelectedMoveToGroup = sub; }); //servers ping FastRealPingCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.FastRealping); }); MixedTestServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Mixedtest); }); TcpingServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Tcping); }, canEditRemove); RealPingServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Realping); }, canEditRemove); UdpTestServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.UdpTest); }, canEditRemove); SpeedServerCmd = ReactiveCommand.CreateFromTask(async () => { await ServerSpeedtest(ESpeedActionType.Speedtest); }, canEditRemove); SortServerResultCmd = ReactiveCommand.CreateFromTask(async () => { await SortServer(nameof(EServerColName.DelayVal)); }); RemoveInvalidServerResultCmd = ReactiveCommand.CreateFromTask(async () => { await RemoveInvalidServerResult(); }); //servers export Export2ClientConfigCmd = ReactiveCommand.CreateFromTask(async () => { await Export2ClientConfigAsync(false); }, canEditRemove); Export2ClientConfigClipboardCmd = ReactiveCommand.CreateFromTask(async () => { await Export2ClientConfigAsync(true); }, canEditRemove); Export2ShareUrlCmd = ReactiveCommand.CreateFromTask(async () => { await Export2ShareUrlAsync(false); }, canEditRemove); Export2ShareUrlBase64Cmd = ReactiveCommand.CreateFromTask(async () => { await Export2ShareUrlAsync(true); }, canEditRemove); Export2InnerUriCmd = ReactiveCommand.CreateFromTask(async () => { await Export2InnerUrlAsync(); }, canEditRemove); //Subscription AddSubCmd = ReactiveCommand.CreateFromTask(async () => { await EditSubAsync(true); }); EditSubCmd = ReactiveCommand.CreateFromTask(async () => { await EditSubAsync(false); }); DeleteSubCmd = ReactiveCommand.CreateFromTask(async () => { await DeleteSubAsync(); }); #endregion WhenAnyValue && ReactiveCommand #region AppEvents AppEvents.DispatcherStatisticsRequested .AsObservable() .ObserveOn(RxSchedulers.MainThreadScheduler) .Subscribe(async result => await UpdateStatistics(result)); #endregion AppEvents _ = Init(); } private async Task Init() { SelectedProfile = new(); SelectedSub = new(); SelectedMoveToGroup = new(); await RefreshSubscriptions(); //await RefreshServers(); } #endregion Init #region Actions private void Reload() { ReloadRequested.Publish(); } public async Task SetSpeedTestResult(SpeedTestResult result) { if (result.IndexId.IsNullOrEmpty()) { NoticeManager.Instance.SendMessageEx(result.Delay); NoticeManager.Instance.Enqueue(result.Delay); return; } var item = ProfileItems.FirstOrDefault(it => it.IndexId == result.IndexId); if (item == null) { return; } if (result.Delay.IsNotEmpty()) { item.Delay = result.Delay.ToInt(); item.DelayVal = result.Delay ?? string.Empty; } if (result.Speed.IsNotEmpty()) { item.SpeedVal = result.Speed ?? string.Empty; } if (result.IpInfo.IsNotEmpty()) { item.IpInfo = result.IpInfo ?? string.Empty; } await Task.CompletedTask; } public async Task UpdateStatistics(ServerSpeedItem update) { if (!_config.GuiItem.EnableStatistics || (update.ProxyUp + update.ProxyDown) <= 0 || DateTime.Now.Second % 3 != 0) { return; } try { var item = ProfileItems.FirstOrDefault(it => it.IndexId == update.IndexId); if (item != null) { item.TodayDown = Utils.HumanFy(update.TodayDown); item.TodayUp = Utils.HumanFy(update.TodayUp); item.TotalDown = Utils.HumanFy(update.TotalDown); item.TotalUp = Utils.HumanFy(update.TotalUp); } } catch { } await Task.CompletedTask; } #endregion Actions #region Servers && Groups private async Task SubSelectedChangedAsync(bool c) { if (!c) { return; } _config.SubIndexId = SelectedSub?.Id; await RefreshServers(); try { await ProfilesFocusInteraction.Handle(RxVoid.Default); } catch (UnhandledInteractionException) { } } private async Task ServerFilterChanged(bool c) { if (!c) { return; } _serverFilter = ServerFilter; if (_serverFilter.IsNullOrEmpty()) { await RefreshServers(); } } public async Task RefreshServers() { RefreshServersRequested.Publish(); // await Task.Delay(200); await Task.CompletedTask; } public async Task RefreshServersBiz() { var lstModel = await GetProfileItemsEx(_config.SubIndexId, _serverFilter); _lstProfile = JsonUtils.Deserialize>(JsonUtils.Serialize(lstModel)) ?? []; ProfileItems.Clear(); ProfileItems.AddRange(lstModel ?? []); if (lstModel?.Count > 0) { ProfileItemModel? selected = null; if (!_pendingSelectIndexId.IsNullOrEmpty()) { selected = lstModel.FirstOrDefault(t => t.IndexId == _pendingSelectIndexId); _pendingSelectIndexId = null; } selected ??= lstModel.FirstOrDefault(t => t.IndexId == _config.IndexId); SelectedProfile = selected ?? lstModel.First(); } try { await DispatcherRefreshServersBizInteraction.Handle(RxVoid.Default); } catch (UnhandledInteractionException) { } } public async Task RefreshSubscriptions() { var subItems = await AppManager.Instance.SubItems(); subItems.Insert(0, new SubItem { Remarks = ResUI.AllGroupServers }); SubItems.Clear(); SubItems.AddRange(subItems); SelectedSub = (_config.SubIndexId.IsNotEmpty() ? subItems.FirstOrDefault(t => t.Id == _config.SubIndexId) : null) ?? subItems.FirstOrDefault(); } public async Task AdjustMainLvColWidth() { await AdjustMainLvColWidthInteraction.Handle(RxVoid.Default); } private async Task?> GetProfileItemsEx(string subid, string filter) { var lstModel = await AppManager.Instance.ProfileModels(_config.SubIndexId, filter); await ConfigHandler.SetDefaultServer(_config, lstModel); var lstServerStat = (_config.GuiItem.EnableStatistics ? StatisticsManager.Instance.ServerStat : null) ?? []; var lstProfileExs = await ProfileExManager.Instance.GetProfileExs(); lstModel = (from t in lstModel join t2 in lstServerStat on t.IndexId equals t2.IndexId into t2b from t22 in t2b.DefaultIfEmpty() join t3 in lstProfileExs on t.IndexId equals t3.IndexId into t3b from t33 in t3b.DefaultIfEmpty() select new ProfileItemModel { IndexId = t.IndexId, ConfigType = t.ConfigType, Remarks = t.Remarks, Address = t.Address, Port = t.Port, //Security = t.Security, Network = t.Network, StreamSecurity = t.StreamSecurity, Subid = t.Subid, SubRemarks = t.SubRemarks, IsActive = t.IndexId == _config.IndexId, Sort = t33?.Sort ?? 0, Delay = t33?.Delay ?? 0, Speed = t33?.Speed ?? 0, DelayVal = t33?.Delay != 0 ? $"{t33?.Delay}" : string.Empty, SpeedVal = t33?.Speed > 0 ? $"{t33?.Speed}" : t33?.Message ?? string.Empty, IpInfo = t33?.IpInfo ?? string.Empty, TodayDown = t22 == null ? "" : Utils.HumanFy(t22.TodayDown), TodayUp = t22 == null ? "" : Utils.HumanFy(t22.TodayUp), TotalDown = t22 == null ? "" : Utils.HumanFy(t22.TotalDown), TotalUp = t22 == null ? "" : Utils.HumanFy(t22.TotalUp) }).OrderBy(t => t.Sort).ToList(); return lstModel; } #endregion Servers && Groups #region Add Servers private async Task?> GetProfileItems(bool latest) { var lstSelected = new List(); if (SelectedProfiles == null || SelectedProfiles.Count <= 0) { return null; } var orderProfiles = SelectedProfiles?.OrderBy(t => t.Sort); if (latest) { lstSelected.AddRange(await AppManager.Instance.GetProfileItemsOrderedByIndexIds(orderProfiles.Select(sp => sp?.IndexId))); } else { lstSelected = JsonUtils.Deserialize>(JsonUtils.Serialize(orderProfiles)); } return lstSelected; } public async Task EditServerAsync() { if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) { return; } var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } var eConfigType = item.ConfigType; bool? ret = false; if (eConfigType is EConfigType.Custom or EConfigType.Outbound) { var addServer2ViewModel = new AddServer2ViewModel(item); ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServer2ViewModel); } else if (eConfigType.IsGroupType()) { var addGroupServerViewModel = new AddGroupServerViewModel(item); ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addGroupServerViewModel); } else { var addServerViewModel = new AddServerViewModel(item); ret = await AppManager.Instance.WindowDialog.ShowDialogAsync(addServerViewModel); } if (ret == true) { await RefreshServers(); if (item.IndexId == _config.IndexId) { Reload(); } } } public async Task RemoveServerAsync() { var lstSelected = await GetProfileItems(true); if (lstSelected == null) { return; } if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } var exists = lstSelected.Exists(t => t.IndexId == _config.IndexId); await ConfigHandler.RemoveServers(_config, lstSelected); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); if (lstSelected.Count == ProfileItems.Count) { ProfileItems.Clear(); } await RefreshServers(); if (exists) { Reload(); } } private async Task RemoveDuplicateServer() { if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } var tuple = await ConfigHandler.DedupServerList(_config, _config.SubIndexId); if (tuple.Item1 > 0 || tuple.Item2 > 0) { await RefreshServers(); Reload(); } NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveDuplicateServerResult, tuple.Item1, tuple.Item2)); } private async Task CopyServer() { var lstSelected = await GetProfileItems(false); if (lstSelected == null) { return; } if (await ConfigHandler.CopyServer(_config, lstSelected) == 0) { await RefreshServers(); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); } } public async Task SetDefaultServer() { if (string.IsNullOrEmpty(SelectedProfile?.IndexId)) { return; } await SetDefaultServer(SelectedProfile.IndexId); } public async Task SetDefaultServer(string? indexId) { if (indexId.IsNullOrEmpty()) { return; } if (indexId == _config.IndexId) { return; } var item = await AppManager.Instance.GetProfileItem(indexId); if (item is null) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } if (await ConfigHandler.SetDefaultServerIndex(_config, indexId) == 0) { await RefreshServers(); Reload(); } } public async Task ShareServerAsync() { var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } var url = FmtHandler.GetShareUri(item); if (url.IsNullOrEmpty()) { return; } await ShareServerInteraction.Handle(url); } private async Task GenGroupAllServer() { var ret = await ConfigHandler.AddGroupAllServer(_config, SelectedSub); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } _pendingSelectIndexId = ret.Data?.ToString(); await RefreshServers(); } private async Task GenGroupRegionServer() { var ret = await ConfigHandler.AddGroupRegionServer(_config, SelectedSub); if (ret.Success != true) { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); return; } var indexIdList = ret.Data as List; _pendingSelectIndexId = indexIdList?.FirstOrDefault(); await RefreshServers(); } public async Task SortServer(string colName) { if (colName.IsNullOrEmpty()) { return; } _dicHeaderSort.TryAdd(colName, true); _dicHeaderSort.TryGetValue(colName, out var asc); if (await ConfigHandler.SortServers(_config, _config.SubIndexId, colName, asc) != 0) { return; } _dicHeaderSort[colName] = !asc; await RefreshServers(); } public async Task RemoveInvalidServerResult() { var count = await ConfigHandler.RemoveInvalidServerResult(_config, _config.SubIndexId); await RefreshServers(); NoticeManager.Instance.Enqueue(string.Format(ResUI.RemoveInvalidServerResultTip, count)); } //move server private async Task MoveToGroup(bool c) { if (!c) { return; } var lstSelected = await GetProfileItems(true); if (lstSelected == null) { return; } await ConfigHandler.MoveToGroup(_config, lstSelected, SelectedMoveToGroup.Id); NoticeManager.Instance.Enqueue(ResUI.OperationSuccess); await RefreshServers(); SelectedMoveToGroup = null; SelectedMoveToGroup = new(); } public async Task MoveServer(EMove eMove) { var item = _lstProfile.FirstOrDefault(t => t.IndexId == SelectedProfile.IndexId); if (item is null) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } var index = _lstProfile.IndexOf(item); if (index < 0) { return; } if (await ConfigHandler.MoveServer(_config, _lstProfile, index, eMove) == 0) { await RefreshServers(); } } public async Task MoveServerTo(int startIndex, ProfileItemModel targetItem) { var targetIndex = ProfileItems.IndexOf(targetItem); if (startIndex >= 0 && targetIndex >= 0 && startIndex != targetIndex) { if (await ConfigHandler.MoveServer(_config, _lstProfile, startIndex, EMove.Position, targetIndex) == 0) { await RefreshServers(); } } } public async Task ServerSpeedtest(ESpeedActionType actionType) { List? lstSelected; if (actionType is ESpeedActionType.Mixedtest or ESpeedActionType.FastRealping) { if (actionType == ESpeedActionType.FastRealping) { actionType = ESpeedActionType.Realping; } lstSelected = JsonUtils.Deserialize>(JsonUtils.Serialize(ProfileItems?.OrderBy(t => t.Sort))); } else { lstSelected = await GetProfileItems(false); } if (lstSelected is null || lstSelected.Count <= 0) { return; } _speedtestService ??= new SpeedtestService(_config, async (SpeedTestResult result) => { RxSchedulers.MainThreadScheduler.Schedule(() => { _ = SetSpeedTestResult(result); }); await Task.CompletedTask; }); _speedtestService?.RunLoop(actionType, lstSelected); } public void ServerSpeedtestStop() { _speedtestService?.ExitLoop(); } private async Task Export2ClientConfigAsync(bool blClipboard) { var item = await AppManager.Instance.GetProfileItem(SelectedProfile.IndexId); if (item is null) { NoticeManager.Instance.Enqueue(ResUI.PleaseSelectServer); return; } var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); if (NoticeManager.Instance.NotifyValidatorResult(validatorResult) && !validatorResult.Success) { return; } if (blClipboard) { var result = await CoreConfigHandler.GenerateClientConfig(context, null); if (result.Success != true) { NoticeManager.Instance.Enqueue(result.Msg); } else { await SetClipboardDataInteraction.Handle((string)result.Data); NoticeManager.Instance.SendMessage(ResUI.OperationSuccess); } } else { await SaveFileDialogInteraction.Handle(item); } } public async Task Export2ClientConfigResult(string fileName, ProfileItem item) { if (fileName.IsNullOrEmpty()) { return; } var (context, validatorResult) = await CoreConfigContextBuilder.Build(_config, item); if (NoticeManager.Instance.NotifyValidatorResult(validatorResult) && !validatorResult.Success) { return; } var result = await CoreConfigHandler.GenerateClientConfig(context, fileName); if (result.Success != true) { NoticeManager.Instance.Enqueue(result.Msg); } else { NoticeManager.Instance.SendMessageAndEnqueue(string.Format(ResUI.SaveClientConfigurationIn, fileName)); } } public async Task Export2ShareUrlAsync(bool blEncode) { var lstSelected = await GetProfileItems(true); if (lstSelected == null) { return; } StringBuilder sb = new(); foreach (var it in lstSelected) { var url = FmtHandler.GetShareUri(it); if (url.IsNullOrEmpty()) { continue; } sb.Append(url); sb.AppendLine(); } if (sb.Length > 0) { if (blEncode) { await SetClipboardDataInteraction.Handle(Utils.Base64Encode(sb.ToString())); } else { await SetClipboardDataInteraction.Handle(sb.ToString()); } NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); } } public async Task Export2InnerUrlAsync() { var lstSelected = await GetProfileItems(true); if (lstSelected == null) { return; } var result = string.Empty; await Task.Run(() => { result = InnerFmt.ToUri(lstSelected); }); if (!result.IsNullOrEmpty()) { await SetClipboardDataInteraction.Handle(result); NoticeManager.Instance.SendMessage(ResUI.BatchExportURLSuccessfully); } else { NoticeManager.Instance.Enqueue(ResUI.OperationFailed); } } #endregion Add Servers #region Subscription private async Task EditSubAsync(bool blNew) { SubItem item; if (blNew) { item = new(); } else { item = await AppManager.Instance.GetSubItem(_config.SubIndexId); if (item is null) { return; } } var subEditViewModel = new SubEditViewModel(item); if (await AppManager.Instance.WindowDialog.ShowDialogAsync(subEditViewModel) == true) { await RefreshSubscriptions(); await SubSelectedChangedAsync(true); } } private async Task DeleteSubAsync() { var item = await AppManager.Instance.GetSubItem(_config.SubIndexId); if (item is null) { return; } if (await ShowYesNoInteraction.Handle(ResUI.RemoveServer) == false) { return; } await ConfigHandler.DeleteSubItem(_config, item.Id); await RefreshSubscriptions(); await SubSelectedChangedAsync(true); } #endregion Subscription }