From aa4ce0cd0eecd6a43c4cd0b87fc3ba3f77f61e98 Mon Sep 17 00:00:00 2001 From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com> Date: Mon, 30 Mar 2026 00:43:09 -0400 Subject: [PATCH 01/10] feat: add game-specific server presets - Persist presets and last-selected preset names per game in JsonSetting - Add ServerPresetModel plus preset name dialog for save/create flow - Add preset picker and save/delete preset actions to the main window - Store clustered state and blocked server keys in each preset - Restore the last applied preset for the current game on load and game switch when the state is still clean - Apply presets through a reset-first flow: clear current rules, switch clustered state if needed, then apply the preset's blocked set - Track blocked server keys in-session so the current state can be saved without reading firewall rules back from the OS - Clear the active preset selection and persisted last-selected preset when manual block/unblock or manual cluster changes dirty the current state - Keep the last preset name as an in-session save suggestion so updating an existing preset does not require retyping it - Use the full current ServerModels list for internal reset paths during preset apply, game switching, and cluster/uncluster transitions while keeping the user-facing Unblock All action filtered - Update the cluster/uncluster button text after preset apply and manual view switches so it matches the actual active clustered state - Add localized preset strings and a disabled empty-state UI when the current game has no presets --- ServerPickerX/Locales/Locale_de-de.axaml | 14 + ServerPickerX/Locales/Locale_en-us.axaml | 14 + ServerPickerX/Locales/Locale_es-es.axaml | 14 + ServerPickerX/Locales/Locale_ja-jp.axaml | 14 + ServerPickerX/Locales/Locale_pl-pl.axaml | 14 + ServerPickerX/Locales/Locale_ru-ru.axaml | 14 + ServerPickerX/Locales/Locale_sv-se.axaml | 14 + ServerPickerX/Locales/Locale_tr-tr.axaml | 14 + ServerPickerX/Locales/Locale_zh-cn.axaml | 14 + ServerPickerX/Models/ServerPresetModel.cs | 15 + .../Services/Settings/JsonSetting.cs | 106 +++++- .../ViewModels/MainWindowViewModel.cs | 359 +++++++++++++++++- ServerPickerX/Views/MainWindow.axaml | 82 +++- ServerPickerX/Views/MainWindow.axaml.cs | 215 +++++++++-- .../Views/UserWindows/PresetNameWindow.axaml | 75 ++++ .../UserWindows/PresetNameWindow.axaml.cs | 45 +++ 16 files changed, 977 insertions(+), 46 deletions(-) create mode 100644 ServerPickerX/Models/ServerPresetModel.cs create mode 100644 ServerPickerX/Views/UserWindows/PresetNameWindow.axaml create mode 100644 ServerPickerX/Views/UserWindows/PresetNameWindow.axaml.cs diff --git a/ServerPickerX/Locales/Locale_de-de.axaml b/ServerPickerX/Locales/Locale_de-de.axaml index 5a980c2..444d60e 100644 --- a/ServerPickerX/Locales/Locale_de-de.axaml +++ b/ServerPickerX/Locales/Locale_de-de.axaml @@ -5,18 +5,32 @@ Server entgruppieren Aktualisieren Nach Servern suchen... + Keine Presets Alle blockieren Ausgewählte blockieren Alle freigeben Ausgewählte freigeben + Preset speichern + Preset löschen + Speichern + Abbrechen Bei Start nach neuen Versionen suchen Brandmauer zurücksetzen Spielmodus auswählen + Preset auswählen Server gruppieren oder entgruppieren Ping aller Server aktualisieren + Aktuell blockierte Server als Preset speichern + Ausgewähltes Preset löschen Diese Aktion wird zuerst alle Server freigeben, um Firewall-Konflikte zu vermeiden. + Preset '{0}' löschen? + Ein Preset mit dem Namen '{0}' existiert für dieses Spiel bereits. Überschreiben? + Der Preset-Name darf nicht leer sein. Sprache auswählen Firewall-Regeln zurücksetzen + Preset speichern + Einen Preset-Namen für das aktuelle Spiel eingeben. + Preset-Name Info Die Serverdaten wurden soeben von Valve aktualisiert! Alle gesperrten Server werden entsperrt, um die neuen Serverdaten zu synchronisieren. Neue Version verfügbar! Zu den Veröffentlichungen? diff --git a/ServerPickerX/Locales/Locale_en-us.axaml b/ServerPickerX/Locales/Locale_en-us.axaml index 39c4570..59a1bb5 100644 --- a/ServerPickerX/Locales/Locale_en-us.axaml +++ b/ServerPickerX/Locales/Locale_en-us.axaml @@ -5,18 +5,32 @@ Uncluster Servers Refresh Search for servers... + No presets Block All Block Selected Unblock All Unblock Selected + Save Preset + Delete Preset + Save + Cancel Check for new version on startup Reset Firewall Select game mode + Select preset Group or ungroup servers Refresh all server ping + Save the current blocked servers as a preset + Delete the selected preset This action will unblock all servers first to prevent firewall conflicts. + Delete the preset '{0}'? + A preset named '{0}' already exists for this game. Overwrite it? + Preset name cannot be empty. Select language Reset firewall rules + Save Preset + Enter a preset name for the current game. + Preset name Info Server data just got updated by Valve! All blocked servers will be unblocked in order to synchronize new server data New version available! Go to releases? diff --git a/ServerPickerX/Locales/Locale_es-es.axaml b/ServerPickerX/Locales/Locale_es-es.axaml index 6879e10..e060948 100644 --- a/ServerPickerX/Locales/Locale_es-es.axaml +++ b/ServerPickerX/Locales/Locale_es-es.axaml @@ -5,18 +5,32 @@ Servidores Uncluster Refrescar Buscar servidores... + No hay presets Bloquear Todo Bloque Seleccionado Desbloquear Todo Desbloquear Seleccionado + Guardar preset + Eliminar preset + Guardar + Cancelar Comprobar si hay nueva versión al iniciar Restablecer el cortafuegos Seleccionar modo de juego + Seleccionar preset Agrupar o desagrupar servidores Actualizar el ping de todos los servidores + Guardar los servidores bloqueados actuales como un preset + Eliminar el preset seleccionado Esta acción desbloqueará primero todos los servidores para evitar conflictos de firewall. + ¿Eliminar el preset '{0}'? + Ya existe un preset llamado '{0}' para este juego. ¿Sobrescribirlo? + El nombre del preset no puede estar vacío. Seleccionar idioma Restablecer reglas del firewall + Guardar preset + Introduce un nombre de preset para el juego actual. + Nombre del preset Información ¡Valve acaba de actualizar los datos del servidor! Todos los servidores bloqueados serán desbloqueados para sincronizar los nuevos datos. ¡Nueva versión disponible! ¿Ir a lanzamientos? diff --git a/ServerPickerX/Locales/Locale_ja-jp.axaml b/ServerPickerX/Locales/Locale_ja-jp.axaml index aff28c6..d65cad4 100644 --- a/ServerPickerX/Locales/Locale_ja-jp.axaml +++ b/ServerPickerX/Locales/Locale_ja-jp.axaml @@ -5,18 +5,32 @@ サーバーのグループ解除 更新 サーバーを検索... + プリセットなし すべてブロック 選択したサーバーをブロック すべて解除 選択したサーバーを解除 + プリセットを保存 + プリセットを削除 + 保存 + キャンセル 起動時に新しいバージョンを確認 ファイアウォールをリセット ゲームモードを選択 + プリセットを選択 サーバーをグループ化またはグループ解除 すべてのサーバーの ping を更新 + 現在ブロック中のサーバーをプリセットとして保存 + 選択したプリセットを削除 この操作は、ファイアウォールの競合を防ぐためにまずすべてのサーバーを解除します。 + プリセット '{0}' を削除しますか? + このゲームには '{0}' という名前のプリセットが既にあります。上書きしますか? + プリセット名は空にできません。 言語を選択 ファイアウォールルールをリセット + プリセットを保存 + 現在のゲーム用のプリセット名を入力してください。 + プリセット名 情報 Valveによってサーバーデータが更新されました!新しいサーバーデータを同期するために、ブロックされていたすべてのサーバーのブロックが解除されます。 新バージョンがリリースされました!リリース一覧へどうぞ。 diff --git a/ServerPickerX/Locales/Locale_pl-pl.axaml b/ServerPickerX/Locales/Locale_pl-pl.axaml index 84b0b97..4e4bd73 100644 --- a/ServerPickerX/Locales/Locale_pl-pl.axaml +++ b/ServerPickerX/Locales/Locale_pl-pl.axaml @@ -5,18 +5,32 @@ Serwery Uncluster Odświeżać Wyszukaj serwery... + Brak presetów Zablokuj wszystko Blok wybrany Odblokuj wszystko Odblokuj wybrane + Zapisz preset + Usuń preset + Zapisz + Anuluj Sprawdź nową wersję podczas uruchamiania Zresetuj zaporę sieciową Wybierz Tryb gry + Wybierz preset Grupowanie lub rozgrupowywanie serwerów Odśwież ping wszystkich serwerów + Zapisz aktualnie zablokowane serwery jako preset + Usuń wybrany preset Ta czynność odblokuje najpierw wszystkie serwery, aby zapobiec konfliktom z zaporą sieciową. + Usunąć preset '{0}'? + Preset o nazwie '{0}' już istnieje dla tej gry. Nadpisać? + Nazwa presetu nie może być pusta. Wybierz język Zresetuj reguły zapory sieciowej + Zapisz preset + Wpisz nazwę presetu dla bieżącej gry. + Nazwa presetu informacje Valve właśnie zaktualizowało dane serwera! Wszystkie zablokowane serwery zostaną odblokowane, aby zsynchronizować nowe dane serwera. Nowa wersja dostępna! Przejdź do wydań? diff --git a/ServerPickerX/Locales/Locale_ru-ru.axaml b/ServerPickerX/Locales/Locale_ru-ru.axaml index 46c46e6..ac98607 100644 --- a/ServerPickerX/Locales/Locale_ru-ru.axaml +++ b/ServerPickerX/Locales/Locale_ru-ru.axaml @@ -5,18 +5,32 @@ Разгруппировать серверы Обновить Поиск серверов... + Нет пресетов Блокировать все Блокировать выбранные Разблокировать все Разблокировать выбранные + Сохранить пресет + Удалить пресет + Сохранить + Отмена Проверять новые версии при запуске Сбросить брандмауэр Выберите режим игры + Выбрать пресет Группировать или разгруппировать серверы Обновить пинг всех серверов + Сохранить текущие заблокированные серверы как пресет + Удалить выбранный пресет Это действие сначала разблокирует все сервера для предотвращения конфликтов брандмауэра. + Удалить пресет '{0}'? + Пресет с именем '{0}' уже существует для этой игры. Перезаписать его? + Имя пресета не может быть пустым. Выберите язык Сбросить правила брандмауэра + Сохранить пресет + Введите имя пресета для текущей игры. + Имя пресета информация Valve обновила данные серверов! Все заблокированные серверы будут разблокированы для синхронизации новых данных. Доступна новая версия! Перейти к релизам? diff --git a/ServerPickerX/Locales/Locale_sv-se.axaml b/ServerPickerX/Locales/Locale_sv-se.axaml index 3f625da..4e905a6 100644 --- a/ServerPickerX/Locales/Locale_sv-se.axaml +++ b/ServerPickerX/Locales/Locale_sv-se.axaml @@ -5,18 +5,32 @@ Avgruppera servrar Uppdatera Sök efter servrar... + Inga förinställningar Blockera alla Blockera valda Avblockera alla Avblockera valda + Spara förinställning + Ta bort förinställning + Spara + Avbryt Kontrollera efter ny version vid start Återställ brandvägg Välj spelläge + Välj förinställning Gruppera eller avgruppera servrar Uppdatera ping för alla servrar + Spara de nuvarande blockerade servrarna som en förinställning + Ta bort den valda förinställningen Detta åtgärder kommer att avblockera alla servrar först för att undvika brandvägskonflikter. + Ta bort förinställningen '{0}'? + En förinställning med namnet '{0}' finns redan för det här spelet. Skriva över den? + Namnet på förinställningen får inte vara tomt. Välj språk Återställ brandväggsregler + Spara förinställning + Ange ett namn på förinställningen för det aktuella spelet. + Namn på förinställning info Serverdata har precis uppdaterats av Valve! Alla blockerade servrar kommer att avblockeras för att synkronisera ny serverdata. Ny version tillgänglig! Gå till utgåvor? diff --git a/ServerPickerX/Locales/Locale_tr-tr.axaml b/ServerPickerX/Locales/Locale_tr-tr.axaml index ae65a1e..bf6316d 100644 --- a/ServerPickerX/Locales/Locale_tr-tr.axaml +++ b/ServerPickerX/Locales/Locale_tr-tr.axaml @@ -5,18 +5,32 @@ Sunucu kümelemeyi kaldır Yenile Sunucu ara... + Ön ayar yok Tümünü engelle Seçili olanları engelle Tüm engellemeleri kaldır Seçili engellemeleri kaldır + Ön ayarı kaydet + Ön ayarı sil + Kaydet + İptal Başlangıçta yeni versiyonları denetle Güvenlik duvarını sıfırla Oyunu seç + Ön ayar seç Sunucuları grupla veya gruplamayı kaldır Tüm sunucu gecikmelerini yenile + Geçerli engellenen sunucuları ön ayar olarak kaydet + Seçili ön ayarı sil Bu işlem, güvenlik duvarı çakışmalarını önlemek amacıyla önce tüm sunucuların engellemelerini kaldıracak. + '{0}' ön ayarı silinsin mi? + Bu oyun için '{0}' adında bir ön ayar zaten var. Üzerine yazılsın mı? + Ön ayar adı boş olamaz. Dil seç Güvenlik duvarı kurallarını sıfırla + Ön ayarı kaydet + Geçerli oyun için bir ön ayar adı girin. + Ön ayar adı Bilgi Sunucu verileri Valve tarafından güncellendi! Yeni sunucu verilerini senkronize etmek için tüm engellenen sunucuların engelleri kaldırılacak Yeni sürüm yayınlandı! Güncelle? diff --git a/ServerPickerX/Locales/Locale_zh-cn.axaml b/ServerPickerX/Locales/Locale_zh-cn.axaml index 897e933..4d0b386 100644 --- a/ServerPickerX/Locales/Locale_zh-cn.axaml +++ b/ServerPickerX/Locales/Locale_zh-cn.axaml @@ -5,18 +5,32 @@ 解组服务器 刷新 搜索服务器... + 无预设 全部阻止 块选定 全部解锁 取消阻止所选内容 + 保存预设 + 删除预设 + 保存 + 取消 启动时检查新版本 重置防火墙 选择游戏模式 + 选择预设 分组或取消分组服务器 刷新所有服务器 ping + 将当前已屏蔽的服务器保存为预设 + 删除所选预设 此操作将首先解除所有服务器的阻止,以防止防火墙冲突。 + 要删除预设“{0}”吗? + 此游戏已存在名为“{0}”的预设。要覆盖吗? + 预设名称不能为空。 选择语言 重置防火墙规则 + 保存预设 + 为当前游戏输入一个预设名称。 + 预设名称 信息 Valve刚刚更新了服务器数据!所有被屏蔽的服务器都将被解除屏蔽,以便同步新的服务器数据。 新版本已发布!前往发布页面? diff --git a/ServerPickerX/Models/ServerPresetModel.cs b/ServerPickerX/Models/ServerPresetModel.cs new file mode 100644 index 0000000..1fb2070 --- /dev/null +++ b/ServerPickerX/Models/ServerPresetModel.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace ServerPickerX.Models +{ + public class ServerPresetModel + { + public string Name { get; set; } = string.Empty; + + public string GameMode { get; set; } = string.Empty; + + public bool IsClustered { get; set; } + + public List BlockedServerKeys { get; set; } = []; + } +} diff --git a/ServerPickerX/Services/Settings/JsonSetting.cs b/ServerPickerX/Services/Settings/JsonSetting.cs index 30e1264..c72cdb5 100644 --- a/ServerPickerX/Services/Settings/JsonSetting.cs +++ b/ServerPickerX/Services/Settings/JsonSetting.cs @@ -1,13 +1,15 @@ - using ServerPickerX.Constants; using ServerPickerX.Helpers; +using ServerPickerX.Models; using ServerPickerX.Services.DependencyInjection; using ServerPickerX.Services.Loggers; using ServerPickerX.Services.MessageBoxes; using ServerPickerX.Services.Settings; using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; +using System.Linq; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -41,6 +43,10 @@ public class JsonSetting : ISetting public virtual bool version_check_on_startup { get; set; } = true; + public virtual List server_presets { get; set; } = []; + + public virtual Dictionary last_selected_preset_names { get; set; } = new(StringComparer.OrdinalIgnoreCase); + [JsonIgnore] public readonly string jsonFilePath = "./settings.json"; @@ -96,6 +102,10 @@ public async Task LoadSettingsAsync() marathon_server_revision = localSettings.marathon_server_revision; is_clustered = localSettings.is_clustered; version_check_on_startup = localSettings.version_check_on_startup; + server_presets = localSettings.server_presets ?? []; + last_selected_preset_names = localSettings.last_selected_preset_names != null + ? new Dictionary(localSettings.last_selected_preset_names, StringComparer.OrdinalIgnoreCase) + : new Dictionary(StringComparer.OrdinalIgnoreCase); } catch (Exception ex) { @@ -192,5 +202,99 @@ public async Task SetLanguageAsync(string language) await this.SaveSettingsAsync(); } + + public string GetLastSelectedPresetNameByGameMode() + { + if (string.IsNullOrWhiteSpace(game_mode)) + { + return string.Empty; + } + + return last_selected_preset_names.TryGetValue(game_mode, out string? presetName) + ? presetName + : string.Empty; + } + + public async Task SetLastSelectedPresetNameByGameModeAsync(string presetName) + { + if (string.IsNullOrWhiteSpace(game_mode)) + { + return; + } + + last_selected_preset_names[game_mode] = presetName; + + await SaveSettingsAsync(); + } + + public async Task ClearLastSelectedPresetNameByGameModeAsync() + { + if (string.IsNullOrWhiteSpace(game_mode)) + { + return; + } + + last_selected_preset_names.Remove(game_mode); + + await SaveSettingsAsync(); + } + + public List GetPresetsByGameMode(string gameMode) + { + string normalizedGameMode = gameMode ?? string.Empty; + + return (server_presets ?? []) + .Where(preset => (preset.GameMode ?? string.Empty).Equals(normalizedGameMode, StringComparison.OrdinalIgnoreCase)) + .OrderBy(preset => preset.Name, StringComparer.OrdinalIgnoreCase) + .ToList(); + } + + public ServerPresetModel? GetPresetByGameMode(string gameMode, string presetName) + { + string normalizedGameMode = gameMode ?? string.Empty; + string normalizedPresetName = presetName ?? string.Empty; + + return (server_presets ?? []).FirstOrDefault(preset => + (preset.GameMode ?? string.Empty).Equals(normalizedGameMode, StringComparison.OrdinalIgnoreCase) && + (preset.Name ?? string.Empty).Equals(normalizedPresetName, StringComparison.OrdinalIgnoreCase)); + } + + public async Task AddOrUpdatePresetAsync(ServerPresetModel serverPreset) + { + server_presets ??= []; + ServerPresetModel? existingPreset = GetPresetByGameMode(serverPreset.GameMode, serverPreset.Name); + List blockedServerKeys = serverPreset.BlockedServerKeys + .Distinct(StringComparer.OrdinalIgnoreCase) + .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) + .ToList(); + + if (existingPreset == null) + { + server_presets.Add(new ServerPresetModel + { + Name = serverPreset.Name, + GameMode = serverPreset.GameMode, + IsClustered = serverPreset.IsClustered, + BlockedServerKeys = blockedServerKeys, + }); + } + else + { + existingPreset.IsClustered = serverPreset.IsClustered; + existingPreset.BlockedServerKeys = blockedServerKeys; + } + + await SaveSettingsAsync(); + } + + public async Task RemovePresetAsync(string gameMode, string presetName) + { + server_presets ??= []; + server_presets.RemoveAll(preset => + preset.GameMode.Equals(gameMode, StringComparison.OrdinalIgnoreCase) && + preset.Name.Equals(presetName, StringComparison.OrdinalIgnoreCase)); + + await SaveSettingsAsync(); + } } } diff --git a/ServerPickerX/ViewModels/MainWindowViewModel.cs b/ServerPickerX/ViewModels/MainWindowViewModel.cs index 15704d7..844ae54 100644 --- a/ServerPickerX/ViewModels/MainWindowViewModel.cs +++ b/ServerPickerX/ViewModels/MainWindowViewModel.cs @@ -23,6 +23,8 @@ public partial class MainWindowViewModel : ViewModelBase { public ObservableCollectionExtended ServerModels { get; set; } = []; + public ObservableCollectionExtended PresetItems { get; set; } = []; + // Property resolved through expression body that react to changes from another observable property public ObservableCollectionExtended FilteredServerModels => string.IsNullOrWhiteSpace(SearchText) @@ -48,21 +50,39 @@ public partial class MainWindowViewModel : ViewModelBase [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsOperationAllowed))] + [NotifyPropertyChangedFor(nameof(CanDeletePreset))] + [NotifyPropertyChangedFor(nameof(CanSelectPresets))] public bool serverModelsInitialized = false; [ObservableProperty] [NotifyPropertyChangedFor(nameof(IsOperationAllowed))] + [NotifyPropertyChangedFor(nameof(CanDeletePreset))] + [NotifyPropertyChangedFor(nameof(CanSelectPresets))] public bool pendingOperation = false; + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanDeletePreset))] + public ServerPresetModel? selectedPreset; + + [ObservableProperty] + [NotifyPropertyChangedFor(nameof(CanSelectPresets))] + public bool hasPresets = false; + // Dependent/Computed prop for main UI buttons `IsEnabled` state public bool IsOperationAllowed => !PendingOperation && ServerModelsInitialized; + public bool CanDeletePreset => IsOperationAllowed && SelectedPreset != null; + + public bool CanSelectPresets => IsOperationAllowed && HasPresets; + private readonly ILoggerService _loggerService; private readonly IMessageBoxService _messageBoxService; private readonly ILocalizationService _localizationService; private readonly IServerDataService _serverDataService; private readonly ISystemFirewallService _systemFirewallService; private readonly JsonSetting _jsonSetting; + private readonly HashSet _blockedServerKeys = new(StringComparer.OrdinalIgnoreCase); + private string _presetNameSuggestion = string.Empty; // Parameterless constructor, allows design previewer to instantiate this class since it doesn't support DI public MainWindowViewModel() @@ -95,40 +115,198 @@ JsonSetting jsonSetting public async Task LoadServersAsync() { + _presetNameSuggestion = string.Empty; ServersLoaded = await _serverDataService.LoadServersAsync(); if (!ServersLoaded) return; - await ClusterUnclusterServersAsync(); + await SetClusterStateAsync(_jsonSetting.is_clustered, false, false); ServerModelsInitialized = true; + + LoadPresetPickerItems(); + await RestoreLastSelectedPresetAsync(); } [RelayCommand] public async Task ClusterUnclusterServersAsync() { - if (!ServersLoaded) return; + await SetClusterStateAsync(!_jsonSetting.is_clustered, true, true); + } - // Update json settings and unblock all servers only after servers are initialized on first load - if (ServerModelsInitialized) + public async Task SetClusterStateAsync(bool isClustered, bool shouldUnblockCurrentServers, bool shouldUpdatePresetSelection) + { + if (!ServersLoaded) { - _jsonSetting.is_clustered = !_jsonSetting.is_clustered; + return; + } - await _jsonSetting.SaveSettingsAsync(); + bool clusterStateChanged = _jsonSetting.is_clustered != isClustered; + + // After initial load, clear the full current view before switching representations + // so clustered/unclustered transitions do not carry stale rules forward + if (shouldUnblockCurrentServers && ServerModelsInitialized && ServerModels.Count > 0) + { + bool unblocked = await PerformOperationAsync( + false, + new ObservableCollection(ServerModels), + false + ); - await UnblockAllAsync(); + if (!unblocked) + { + return; + } } - ServerData serverData = _serverDataService.GetServerData(); + if (clusterStateChanged) + { + _jsonSetting.is_clustered = isClustered; - List serverModels = _jsonSetting.is_clustered ? - serverData.ClusteredServers : serverData.UnclusteredServers; + await _jsonSetting.SaveSettingsAsync(); + } - ServerModels.Clear(); + ServerData serverData = _serverDataService.GetServerData(); + List serverModels = _jsonSetting.is_clustered + ? serverData.ClusteredServers + : serverData.UnclusteredServers; + ServerModels.Clear(); ServerModels.AddRange(serverModels); PingServers(serverModels); + + if (shouldUpdatePresetSelection) + { + if (clusterStateChanged) + { + await MarkPresetSelectionDirtyAsync(); + } + } + } + + public List GetCurrentGamePresets() + { + return _jsonSetting.GetPresetsByGameMode(_jsonSetting.game_mode); + } + + public ServerPresetModel? GetCurrentGamePreset(string presetName) + { + return _jsonSetting.GetPresetByGameMode(_jsonSetting.game_mode, presetName); + } + + public void LoadPresetPickerItems() + { + string? selectedPresetName = SelectedPreset?.Name; + List presetItems = GetCurrentGamePresets(); + + PresetItems.Clear(); + + if (presetItems.Count == 0) + { + HasPresets = false; + ClearSelectedPreset(); + return; + } + + HasPresets = true; + PresetItems.AddRange(presetItems); + + if (!string.IsNullOrWhiteSpace(selectedPresetName)) + { + SelectPresetByName(selectedPresetName); + return; + } + + ClearSelectedPreset(); + } + + public void SelectPresetByName(string presetName) + { + SelectedPreset = PresetItems.FirstOrDefault(preset => + preset.Name.Equals(presetName, StringComparison.OrdinalIgnoreCase)); + } + + public async Task SavePresetAsync(string presetName) + { + ServerPresetModel serverPreset = new() + { + Name = presetName.Trim(), + GameMode = _jsonSetting.game_mode, + IsClustered = _jsonSetting.is_clustered, + BlockedServerKeys = _blockedServerKeys + .OrderBy(key => key, StringComparer.OrdinalIgnoreCase) + .ToList(), + }; + + await _jsonSetting.AddOrUpdatePresetAsync(serverPreset); + await _jsonSetting.SetLastSelectedPresetNameByGameModeAsync(serverPreset.Name); + + _presetNameSuggestion = serverPreset.Name; + LoadPresetPickerItems(); + SelectPresetByName(serverPreset.Name); + } + + public string GetPresetNameSuggestion() + { + return SelectedPreset?.Name ?? _presetNameSuggestion; + } + + public bool IsSuggestedPresetName(string presetName) + { + return GetPresetNameSuggestion().Equals(presetName, StringComparison.OrdinalIgnoreCase); + } + + public async Task DeleteSelectedPresetAsync() + { + if (SelectedPreset == null) + { + return; + } + + string deletedPresetName = SelectedPreset.Name; + + await _jsonSetting.RemovePresetAsync(_jsonSetting.game_mode, deletedPresetName); + + if (_jsonSetting.GetLastSelectedPresetNameByGameMode().Equals(deletedPresetName, StringComparison.OrdinalIgnoreCase)) + { + await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync(); + } + + if (GetPresetNameSuggestion().Equals(deletedPresetName, StringComparison.OrdinalIgnoreCase)) + { + _presetNameSuggestion = string.Empty; + } + + LoadPresetPickerItems(); + ClearSelectedPreset(); + } + + public async Task ApplyPresetAsync(ServerPresetModel serverPreset) + { + if (!ServersLoaded) + { + return false; + } + + if (!serverPreset.GameMode.Equals(_jsonSetting.game_mode, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + bool presetApplied = await ApplyPresetWithResetAsync(serverPreset); + + if (!presetApplied) + { + return false; + } + + ReplaceBlockedServerKeysFromPreset(serverPreset); + await _jsonSetting.SetLastSelectedPresetNameByGameModeAsync(serverPreset.Name); + _presetNameSuggestion = serverPreset.Name; + SelectPresetByName(serverPreset.Name); + + return true; } [RelayCommand] @@ -202,6 +380,15 @@ public async Task UnblockAllAsync() return await PerformOperationAsync(false, FilteredServerModels); } + public async Task UnblockCurrentGameServersAsync() + { + if (ServerModels.Count == 0) + { + return false; + } + + return await PerformOperationAsync(false, new ObservableCollection(ServerModels), false); + } [RelayCommand] public async Task UnblockSelectedAsync(IList selectedServers) @@ -221,7 +408,11 @@ await _messageBoxService.ShowMessageBoxAsync( return await PerformOperationAsync(false, serverModels); } - public async Task PerformOperationAsync(bool shouldBlock, ObservableCollection serverModels) + public async Task PerformOperationAsync( + bool shouldBlock, + ObservableCollection serverModels, + bool shouldUpdatePresetSelection = true + ) { if (PendingOperation) { @@ -254,8 +445,17 @@ await _messageBoxService.ShowMessageBoxAsync( await _loggerService.LogInfoAsync("Servers unblocked successfully"); } + TrackBlockedServerKeys(serverModels, shouldBlock); + + if (shouldUpdatePresetSelection) + { + await MarkPresetSelectionDirtyAsync(); + } + // Ping servers (parallel operation) PingServers(serverModels); + + return true; } catch (Exception ex) { @@ -268,11 +468,11 @@ await _messageBoxService.ShowMessageBoxAsync( return false; } - - PendingOperation = false; - ShowProgressBar = false; - - return true; + finally + { + PendingOperation = false; + ShowProgressBar = false; + } } public IServerDataService GetServerDataService() @@ -280,5 +480,130 @@ public IServerDataService GetServerDataService() return _serverDataService; } + private void TrackBlockedServerKeys(IEnumerable serverModels, bool shouldBlock) + { + foreach (ServerModel serverModel in serverModels) + { + string serverKey = GetServerKey(serverModel); + + if (shouldBlock) + { + _blockedServerKeys.Add(serverKey); + } + else + { + _blockedServerKeys.Remove(serverKey); + } + } + } + + private string GetServerKey(ServerModel serverModel) + { + return _jsonSetting.is_clustered + ? serverModel.Description + : serverModel.Name; + } + + private async Task RestoreLastSelectedPresetAsync() + { + if (!HasPresets) + { + await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync(); + + ClearSelectedPreset(); + return; + } + + string lastSelectedPresetName = _jsonSetting.GetLastSelectedPresetNameByGameMode(); + + if (string.IsNullOrWhiteSpace(lastSelectedPresetName)) + { + ClearSelectedPreset(); + return; + } + + ServerPresetModel? lastSelectedPreset = GetCurrentGamePreset(lastSelectedPresetName); + + if (lastSelectedPreset == null) + { + await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync(); + ClearSelectedPreset(); + return; + } + + bool restored = await ApplyPresetAsync(lastSelectedPreset); + + if (!restored) + { + ClearSelectedPreset(); + } + } + + private async Task ApplyPresetWithResetAsync(ServerPresetModel serverPreset) + { + if (ServerModels.Count > 0) + { + bool unblocked = await PerformOperationAsync( + false, + new ObservableCollection(ServerModels), + false + ); + + if (!unblocked) + { + return false; + } + } + + await SetClusterStateAsync(serverPreset.IsClustered, false, false); + + ObservableCollection matchingServerModels = GetMatchingServerModels(serverPreset); + + if (matchingServerModels.Count == 0) + { + return true; + } + + return await PerformOperationAsync(true, matchingServerModels, false); + } + + private ObservableCollection GetMatchingServerModels(ServerPresetModel serverPreset) + { + return new ObservableCollection( + ServerModels.Where(serverModel => + (serverPreset.BlockedServerKeys ?? []) + .Contains(GetServerKey(serverModel), StringComparer.OrdinalIgnoreCase)) + ); + } + + private void ReplaceBlockedServerKeysFromPreset(ServerPresetModel serverPreset) + { + ObservableCollection matchingServerModels = GetMatchingServerModels(serverPreset); + + _blockedServerKeys.Clear(); + + foreach (string blockedServerKey in matchingServerModels.Select(GetServerKey)) + { + _blockedServerKeys.Add(blockedServerKey); + } + } + + private void ClearSelectedPreset() + { + SelectedPreset = null; + } + + private async Task MarkPresetSelectionDirtyAsync() + { + if (SelectedPreset != null) + { + _presetNameSuggestion = SelectedPreset.Name; + } + + await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync(); + + ClearSelectedPreset(); + } + } } diff --git a/ServerPickerX/Views/MainWindow.axaml b/ServerPickerX/Views/MainWindow.axaml index 76bcde1..167d467 100644 --- a/ServerPickerX/Views/MainWindow.axaml +++ b/ServerPickerX/Views/MainWindow.axaml @@ -10,8 +10,8 @@ mc:Ignorable="d" x:Class="ServerPickerX.Views.MainWindow" x:DataType="vm:MainWindowViewModel" - Width="850" - MinWidth="850" + Width="980" + MinWidth="980" MaxWidth="1280" Height="640" MinHeight="500" @@ -74,8 +74,14 @@ - - + + + + + + + + + + + + + + + + + + + + - + +