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 @@
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
From c698e423b803ce51afd833a0ae0788797d3e1d95 Mon Sep 17 00:00:00 2001
From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com>
Date: Tue, 31 Mar 2026 23:02:37 -0400
Subject: [PATCH 03/10] fix: reset and prune stale preset entries on server
revision changes
---
ServerPickerX/Locales/Locale_de-de.axaml | 2 +-
ServerPickerX/Locales/Locale_en-us.axaml | 2 +-
ServerPickerX/Locales/Locale_es-es.axaml | 2 +-
ServerPickerX/Locales/Locale_ja-jp.axaml | 2 +-
ServerPickerX/Locales/Locale_pl-pl.axaml | 2 +-
ServerPickerX/Locales/Locale_ru-ru.axaml | 2 +-
ServerPickerX/Locales/Locale_sv-se.axaml | 2 +-
ServerPickerX/Locales/Locale_tr-tr.axaml | 4 +-
ServerPickerX/Locales/Locale_zh-cn.axaml | 2 +-
.../CS2PerfectWorldServerDataService.cs | 14 +---
.../Services/Servers/CS2ServerDataService.cs | 12 +--
.../Servers/DeadLockServerDataService.cs | 12 +--
.../Servers/MarathonServerDataService.cs | 12 +--
.../Services/Settings/JsonSetting.cs | 55 +++++++++++++
.../ViewModels/MainWindowViewModel.cs | 78 +++++++++++++++++--
ServerPickerX/Views/MainWindow.axaml.cs | 43 ++++++++--
16 files changed, 182 insertions(+), 64 deletions(-)
diff --git a/ServerPickerX/Locales/Locale_de-de.axaml b/ServerPickerX/Locales/Locale_de-de.axaml
index 444d60e..ee7b083 100644
--- a/ServerPickerX/Locales/Locale_de-de.axaml
+++ b/ServerPickerX/Locales/Locale_de-de.axaml
@@ -32,7 +32,7 @@
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.
+ Die Serverdaten wurden gerade von Valve aktualisiert! Alle blockierten Server werden entsperrt, um die neuen Serverdaten zu synchronisieren. Preset-Einträge für Server, die nicht mehr existieren, werden entfernt, und das zuletzt ausgewählte Preset wird erneut angewendet, falls verfügbar.
Neue Version verfügbar! Zu den Veröffentlichungen?
Hey! Bitte wähle mindestens einen Server zum Blockieren aus.
Bitte wählen Sie mindestens einen Server aus, um die Entsperrung aufzuheben.
diff --git a/ServerPickerX/Locales/Locale_en-us.axaml b/ServerPickerX/Locales/Locale_en-us.axaml
index 59a1bb5..333fe78 100644
--- a/ServerPickerX/Locales/Locale_en-us.axaml
+++ b/ServerPickerX/Locales/Locale_en-us.axaml
@@ -32,7 +32,7 @@
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
+ Server data just got updated by Valve! All blocked servers will be unblocked in order to synchronize new server data. Preset entries for servers that no longer exist will be removed, and the last selected preset will be reapplied if available.
New version available! Go to releases?
Hey! Please select at least one server to block
Hey! Please select at least one server to unblock
diff --git a/ServerPickerX/Locales/Locale_es-es.axaml b/ServerPickerX/Locales/Locale_es-es.axaml
index e060948..bd4c311 100644
--- a/ServerPickerX/Locales/Locale_es-es.axaml
+++ b/ServerPickerX/Locales/Locale_es-es.axaml
@@ -32,7 +32,7 @@
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.
+ Valve acaba de actualizar los datos del servidor. Todos los servidores bloqueados se desbloquearán para sincronizar los nuevos datos. Se eliminarán las entradas del preset correspondientes a servidores que ya no existen y, si está disponible, se volverá a aplicar el último preset seleccionado.
¡Nueva versión disponible! ¿Ir a lanzamientos?
Por favor, selecciona al menos un servidor para bloquear.
Seleccione al menos un servidor para desbloquear.
diff --git a/ServerPickerX/Locales/Locale_ja-jp.axaml b/ServerPickerX/Locales/Locale_ja-jp.axaml
index d65cad4..8c7d27b 100644
--- a/ServerPickerX/Locales/Locale_ja-jp.axaml
+++ b/ServerPickerX/Locales/Locale_ja-jp.axaml
@@ -32,7 +32,7 @@
現在のゲーム用のプリセット名を入力してください。
プリセット名
情報
- Valveによってサーバーデータが更新されました!新しいサーバーデータを同期するために、ブロックされていたすべてのサーバーのブロックが解除されます。
+ Valve によってサーバーデータが更新されました。新しいサーバーデータと同期するために、ブロックされていたすべてのサーバーのブロックが解除されます。存在しなくなったサーバーのプリセット項目は削除され、可能であれば最後に選択したプリセットが再適用されます。
新バージョンがリリースされました!リリース一覧へどうぞ。
こんにちは!ブロックするサーバーを少なくとも1つ選択してください。
ブロックを解除したいサーバーを少なくとも1つ選択してください。
diff --git a/ServerPickerX/Locales/Locale_pl-pl.axaml b/ServerPickerX/Locales/Locale_pl-pl.axaml
index 4e4bd73..37129ab 100644
--- a/ServerPickerX/Locales/Locale_pl-pl.axaml
+++ b/ServerPickerX/Locales/Locale_pl-pl.axaml
@@ -32,7 +32,7 @@
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.
+ Valve właśnie zaktualizowało dane serwerów. Wszystkie zablokowane serwery zostaną odblokowane, aby zsynchronizować nowe dane serwera. Wpisy presetów dla serwerów, które już nie istnieją, zostaną usunięte, a ostatnio wybrany preset zostanie zastosowany ponownie, jeśli jest dostępny.
Nowa wersja dostępna! Przejdź do wydań?
Hej! Wybierz co najmniej jeden serwer do zablokowania
Hej! Wybierz co najmniej jeden serwer do odblokowania
diff --git a/ServerPickerX/Locales/Locale_ru-ru.axaml b/ServerPickerX/Locales/Locale_ru-ru.axaml
index ac98607..3298ec9 100644
--- a/ServerPickerX/Locales/Locale_ru-ru.axaml
+++ b/ServerPickerX/Locales/Locale_ru-ru.axaml
@@ -32,7 +32,7 @@
Введите имя пресета для текущей игры.
Имя пресета
информация
- Valve обновила данные серверов! Все заблокированные серверы будут разблокированы для синхронизации новых данных.
+ Valve обновила данные серверов. Все заблокированные серверы будут разблокированы для синхронизации новых данных. Записи пресета для серверов, которые больше не существуют, будут удалены, а последний выбранный пресет будет применен снова, если он доступен.
Доступна новая версия! Перейти к релизам?
Пожалуйста, выберите хотя бы один сервер для блокировки.
Пожалуйста, выберите хотя бы один сервер для разблокировки.
diff --git a/ServerPickerX/Locales/Locale_sv-se.axaml b/ServerPickerX/Locales/Locale_sv-se.axaml
index 4e905a6..3ec510c 100644
--- a/ServerPickerX/Locales/Locale_sv-se.axaml
+++ b/ServerPickerX/Locales/Locale_sv-se.axaml
@@ -32,7 +32,7 @@
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.
+ Serverdata har just uppdaterats av Valve. Alla blockerade servrar kommer att avblockeras för att synkronisera ny serverdata. Förinställningsposter för servrar som inte längre finns kommer att tas bort, och den senast valda förinställningen kommer att tillämpas igen om den fortfarande finns.
Ny version tillgänglig! Gå till utgåvor?
Välj minst en server att blockera
Välj minst en server att avblockera.
diff --git a/ServerPickerX/Locales/Locale_tr-tr.axaml b/ServerPickerX/Locales/Locale_tr-tr.axaml
index bf6316d..619faa1 100644
--- a/ServerPickerX/Locales/Locale_tr-tr.axaml
+++ b/ServerPickerX/Locales/Locale_tr-tr.axaml
@@ -32,11 +32,11 @@
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
+ Sunucu verileri Valve tarafından güncellendi! Yeni sunucu verilerini senkronize etmek için tüm engellenen sunucuların engelleri kaldırılacak. Artık mevcut olmayan sunuculara ait preset girdileri kaldırılacak ve varsa son seçilen preset yeniden uygulanacak.
Yeni sürüm yayınlandı! Güncelle?
Lütfen engellenecek en az bir sunucu seç
Lütfen engeli kaldırılacak en az bir sunucu seçin
Zaten bekleyen bir işlem var. Lütfen bekleyin...
Bu işlem güvenlik duvarını sıfırlayacak. Onaylıyor musun?
Güvenlik duvarı başarıyla sıfırlandı!
-
\ No newline at end of file
+
diff --git a/ServerPickerX/Locales/Locale_zh-cn.axaml b/ServerPickerX/Locales/Locale_zh-cn.axaml
index 4d0b386..40d3bb8 100644
--- a/ServerPickerX/Locales/Locale_zh-cn.axaml
+++ b/ServerPickerX/Locales/Locale_zh-cn.axaml
@@ -32,7 +32,7 @@
为当前游戏输入一个预设名称。
预设名称
信息
- Valve刚刚更新了服务器数据!所有被屏蔽的服务器都将被解除屏蔽,以便同步新的服务器数据。
+ Valve 刚刚更新了服务器数据。所有被屏蔽的服务器都将被解除屏蔽,以便同步新的服务器数据。对于已不存在的服务器,其预设条目将被移除,并且如果可用,最后选中的预设将被重新应用。
新版本已发布!前往发布页面?
嘿!请至少选择一个服务器进行屏蔽。
嘿!请至少选择一个服务器进行解锁。
diff --git a/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs b/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs
index aaed89d..b4a56b3 100644
--- a/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs
+++ b/ServerPickerX/Services/Servers/CS2PerfectWorldServerDataService.cs
@@ -1,4 +1,3 @@
-using ServerPickerX.Settings;
using ServerPickerX.Models;
using System;
using System.Collections.Generic;
@@ -14,8 +13,7 @@ namespace ServerPickerX.Services.Servers
public class CS2PerfectWorldServerDataService(
ILoggerService _logger,
IMessageBoxService _messageBoxService,
- HttpClient _httpClient,
- JsonSetting _jsonSettings
+ HttpClient _httpClient
) : IServerDataService
{
private ServerData _serverData = new();
@@ -47,14 +45,6 @@ public async Task LoadServersAsync()
_serverData.Revision = revision;
- // Update settings if app is initialized for the first time
- if (_jsonSettings.cs2_server_revision == "-1")
- {
- _jsonSettings.cs2_server_revision = revision;
-
- await _jsonSettings.SaveSettingsAsync();
- }
-
ProcessServers(mainJson, _serverData);
}
catch (Exception ex)
@@ -158,4 +148,4 @@ private List GetWhiteListedServerKeywords()
return ["China"];
}
}
-}
\ No newline at end of file
+}
diff --git a/ServerPickerX/Services/Servers/CS2ServerDataService.cs b/ServerPickerX/Services/Servers/CS2ServerDataService.cs
index b3b928b..e29bc45 100644
--- a/ServerPickerX/Services/Servers/CS2ServerDataService.cs
+++ b/ServerPickerX/Services/Servers/CS2ServerDataService.cs
@@ -1,4 +1,3 @@
-using ServerPickerX.Settings;
using ServerPickerX.Models;
using System;
using System.Collections.Generic;
@@ -14,8 +13,7 @@ namespace ServerPickerX.Services.Servers
public class CS2ServerDataService(
ILoggerService _logger,
IMessageBoxService _messageBoxService,
- HttpClient _httpClient,
- JsonSetting _jsonSettings
+ HttpClient _httpClient
) : IServerDataService
{
private ServerData _serverData = new();
@@ -47,12 +45,6 @@ public async Task LoadServersAsync()
_serverData.Revision = revision;
- // Update settings if app is initialized for the first time
- if (_jsonSettings.cs2_server_revision == "-1")
- {
- await _jsonSettings.SetRevisionByGameModeAsync(revision);
- }
-
ProcessServers(mainJson, _serverData);
}
catch (Exception ex)
@@ -155,4 +147,4 @@ private List GetExcludedServerKeywords()
return ["China"];
}
}
-}
\ No newline at end of file
+}
diff --git a/ServerPickerX/Services/Servers/DeadLockServerDataService.cs b/ServerPickerX/Services/Servers/DeadLockServerDataService.cs
index f139ae7..d17beaf 100644
--- a/ServerPickerX/Services/Servers/DeadLockServerDataService.cs
+++ b/ServerPickerX/Services/Servers/DeadLockServerDataService.cs
@@ -1,4 +1,3 @@
-using ServerPickerX.Settings;
using ServerPickerX.Models;
using System;
using System.Collections.Generic;
@@ -14,8 +13,7 @@ namespace ServerPickerX.Services.Servers
public class DeadLockServerDataService(
ILoggerService _logger,
IMessageBoxService _messageBoxService,
- HttpClient _httpClient,
- JsonSetting _jsonSettings
+ HttpClient _httpClient
) : IServerDataService
{
private ServerData _serverData = new();
@@ -47,12 +45,6 @@ public async Task LoadServersAsync()
_serverData.Revision = revision;
- // Update settings if app is initialized for the first time
- if (_jsonSettings.deadlock_server_revision == "-1")
- {
- await _jsonSettings.SetRevisionByGameModeAsync(revision);
- }
-
ProcessServers(mainJson, _serverData);
}
catch (Exception ex)
@@ -142,4 +134,4 @@ public List GetClusterKeywords()
};
}
}
-}
\ No newline at end of file
+}
diff --git a/ServerPickerX/Services/Servers/MarathonServerDataService.cs b/ServerPickerX/Services/Servers/MarathonServerDataService.cs
index b549a28..a8cb480 100644
--- a/ServerPickerX/Services/Servers/MarathonServerDataService.cs
+++ b/ServerPickerX/Services/Servers/MarathonServerDataService.cs
@@ -1,4 +1,3 @@
-using ServerPickerX.Settings;
using ServerPickerX.Models;
using System;
using System.Collections.Generic;
@@ -14,8 +13,7 @@ namespace ServerPickerX.Services.Servers
public class MarathonServerDataService(
ILoggerService _logger,
IMessageBoxService _messageBoxService,
- HttpClient _httpClient,
- JsonSetting _jsonSettings
+ HttpClient _httpClient
) : IServerDataService
{
private ServerData _serverData = new();
@@ -47,12 +45,6 @@ public async Task LoadServersAsync()
_serverData.Revision = revision;
- // Update settings if app is initialized for the first time
- if (_jsonSettings.marathon_server_revision == "-1")
- {
- await _jsonSettings.SetRevisionByGameModeAsync(revision);
- }
-
ProcessServers(mainJson, _serverData);
}
catch (Exception ex)
@@ -143,4 +135,4 @@ public List GetClusterKeywords()
];
}
}
-}
\ No newline at end of file
+}
diff --git a/ServerPickerX/Services/Settings/JsonSetting.cs b/ServerPickerX/Services/Settings/JsonSetting.cs
index c72cdb5..9baedae 100644
--- a/ServerPickerX/Services/Settings/JsonSetting.cs
+++ b/ServerPickerX/Services/Settings/JsonSetting.cs
@@ -296,5 +296,60 @@ public async Task RemovePresetAsync(string gameMode, string presetName)
await SaveSettingsAsync();
}
+
+ public async Task PrunePresetEntriesByGameModeAsync(
+ string gameMode,
+ HashSet clusteredServerKeys,
+ HashSet unclusteredServerKeys
+ )
+ {
+ if (string.IsNullOrWhiteSpace(gameMode))
+ {
+ return false;
+ }
+
+ server_presets ??= [];
+
+ bool presetsChanged = false;
+
+ foreach (ServerPresetModel preset in server_presets.Where(preset =>
+ (preset.GameMode ?? string.Empty).Equals(gameMode, StringComparison.OrdinalIgnoreCase)))
+ {
+ HashSet validServerKeys = preset.IsClustered
+ ? clusteredServerKeys
+ : unclusteredServerKeys;
+
+ List prunedServerKeys = (preset.BlockedServerKeys ?? [])
+ .Where(serverKey => !string.IsNullOrWhiteSpace(serverKey) && validServerKeys.Contains(serverKey))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(serverKey => serverKey, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ List currentServerKeys = (preset.BlockedServerKeys ?? [])
+ .Where(serverKey => !string.IsNullOrWhiteSpace(serverKey))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(serverKey => serverKey, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ if (currentServerKeys.SequenceEqual(prunedServerKeys, StringComparer.OrdinalIgnoreCase))
+ {
+ continue;
+ }
+
+ preset.BlockedServerKeys = prunedServerKeys;
+ presetsChanged = true;
+ }
+
+ if (!presetsChanged)
+ {
+ return false;
+ }
+
+ await SaveSettingsAsync();
+
+ return true;
+ }
+
+
}
}
diff --git a/ServerPickerX/ViewModels/MainWindowViewModel.cs b/ServerPickerX/ViewModels/MainWindowViewModel.cs
index 844ae54..2fcff85 100644
--- a/ServerPickerX/ViewModels/MainWindowViewModel.cs
+++ b/ServerPickerX/ViewModels/MainWindowViewModel.cs
@@ -1,6 +1,7 @@
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using MsBox.Avalonia.Enums;
+using ServerPickerX.Constants;
using ServerPickerX.Extensions;
using ServerPickerX.Models;
using ServerPickerX.Services.DependencyInjection;
@@ -123,9 +124,6 @@ public async Task LoadServersAsync()
await SetClusterStateAsync(_jsonSetting.is_clustered, false, false);
ServerModelsInitialized = true;
-
- LoadPresetPickerItems();
- await RestoreLastSelectedPresetAsync();
}
[RelayCommand]
@@ -384,7 +382,7 @@ public async Task UnblockCurrentGameServersAsync()
{
if (ServerModels.Count == 0)
{
- return false;
+ return true;
}
return await PerformOperationAsync(false, new ObservableCollection(ServerModels), false);
@@ -480,6 +478,74 @@ public IServerDataService GetServerDataService()
return _serverDataService;
}
+ public async Task PruneCurrentGamePresetEntriesAsync()
+ {
+ if (!ServersLoaded)
+ {
+ return false;
+ }
+
+ return await PrunePresetEntriesAsync(_jsonSetting.game_mode, _serverDataService.GetServerData());
+ }
+
+ public async Task PruneCounterStrikeFamilyPresetEntriesAsync()
+ {
+ // CS2 and Perfect World share one revision bucket, but their filtered server sets differ.
+ // When either mode syncs, prune the sibling mode too before marking the shared revision current.
+ if (_jsonSetting.game_mode == GameModes.CounterStrike2)
+ {
+ CS2PerfectWorldServerDataService perfectWorldServerDataService =
+ ServiceLocator.GetRequiredService();
+
+ if (!await perfectWorldServerDataService.LoadServersAsync())
+ {
+ return false;
+ }
+
+ await PrunePresetEntriesAsync(
+ GameModes.CounterStrike2PerfectWorld,
+ perfectWorldServerDataService.GetServerData()
+ );
+
+ return true;
+ }
+
+ CS2ServerDataService counterStrikeServerDataService =
+ ServiceLocator.GetRequiredService();
+
+ if (!await counterStrikeServerDataService.LoadServersAsync())
+ {
+ return false;
+ }
+
+ await PrunePresetEntriesAsync(
+ GameModes.CounterStrike2,
+ counterStrikeServerDataService.GetServerData()
+ );
+
+ return true;
+ }
+
+ public async Task PrunePresetEntriesAsync(string gameMode, ServerData serverData)
+ {
+ HashSet clusteredServerKeys = serverData.ClusteredServers
+ .Select(serverModel => serverModel.Description)
+ .Where(serverKey => !string.IsNullOrWhiteSpace(serverKey))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ HashSet unclusteredServerKeys = serverData.UnclusteredServers
+ .Select(serverModel => serverModel.Name)
+ .Where(serverKey => !string.IsNullOrWhiteSpace(serverKey))
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ bool presetsPruned = await _jsonSetting.PrunePresetEntriesByGameModeAsync(
+ gameMode,
+ clusteredServerKeys,
+ unclusteredServerKeys
+ );
+
+ return presetsPruned;
+ }
+
private void TrackBlockedServerKeys(IEnumerable serverModels, bool shouldBlock)
{
foreach (ServerModel serverModel in serverModels)
@@ -504,7 +570,7 @@ private string GetServerKey(ServerModel serverModel)
: serverModel.Name;
}
- private async Task RestoreLastSelectedPresetAsync()
+ public async Task RestoreLastSelectedPresetAsync()
{
if (!HasPresets)
{
@@ -571,7 +637,7 @@ private ObservableCollection GetMatchingServerModels(ServerPresetMo
{
return new ObservableCollection(
ServerModels.Where(serverModel =>
- (serverPreset.BlockedServerKeys ?? [])
+ serverPreset.BlockedServerKeys
.Contains(GetServerKey(serverModel), StringComparer.OrdinalIgnoreCase))
);
}
diff --git a/ServerPickerX/Views/MainWindow.axaml.cs b/ServerPickerX/Views/MainWindow.axaml.cs
index 44c094b..68ce0c8 100644
--- a/ServerPickerX/Views/MainWindow.axaml.cs
+++ b/ServerPickerX/Views/MainWindow.axaml.cs
@@ -170,7 +170,7 @@ private async void SavePresetBtn_Click(object? sender, Avalonia.Interactivity.Ro
return;
}
- presetName = presetName?.Trim() ?? string.Empty;
+ presetName = presetName.Trim();
if (string.IsNullOrWhiteSpace(presetName))
{
@@ -259,14 +259,16 @@ public async Task InitializeApp()
DataContext = vm;
- ConfigurePresetControls(vm);
- RefreshClusterButtonContent();
-
if (vm.ServersLoaded)
{
await SyncServersAsync(vm);
+ vm.LoadPresetPickerItems();
+ await vm.RestoreLastSelectedPresetAsync();
}
+ ConfigurePresetControls(vm);
+ RefreshClusterButtonContent();
+
await _versionService.CheckVersionAsync();
}
@@ -314,24 +316,53 @@ private void ConfigurePresetControls(MainWindowViewModel vm)
private async Task SyncServersAsync(MainWindowViewModel vm)
{
- // If Steam SDR API data got updated, sync the changes
var localRevision = await _jsonSetting.GetRevisionByGameModeAsync();
var fetchedRevision = vm.GetServerDataService().GetFetchedRevision();
+ bool isCounterStrikeFamilyGame = _jsonSetting.game_mode is
+ GameModes.CounterStrike2 or GameModes.CounterStrike2PerfectWorld;
+ bool hasAffectedPresets = isCounterStrikeFamilyGame
+ ? _jsonSetting.GetPresetsByGameMode(GameModes.CounterStrike2).Count > 0 ||
+ _jsonSetting.GetPresetsByGameMode(GameModes.CounterStrike2PerfectWorld).Count > 0
+ : _jsonSetting.GetPresetsByGameMode(_jsonSetting.game_mode).Count > 0;
+
+ // Store the initial revision without a reset when this game has no saved presets yet.
+ if (localRevision == "-1" && !hasAffectedPresets)
+ {
+ await _jsonSetting.SetRevisionByGameModeAsync(fetchedRevision);
+ return;
+ }
+
// Skip server unblocking and revision sync if local revision is equal to fetched revision
if (localRevision == fetchedRevision)
{
return;
}
+ // This only happens on successful load and sync on startup or game switch
await _messageBoxService.ShowMessageBoxAsync(
_localizationService.GetLocaleValue("MessageBoxInfoTitle"),
_localizationService.GetLocaleValue("SyncServersUnblockAllDialogue"),
MsBox.Avalonia.Enums.Icon.Setting
);
- await vm.UnblockCurrentGameServersAsync();
+ bool unblocked = await vm.UnblockCurrentGameServersAsync();
+
+ if (!unblocked)
+ {
+ return;
+ }
+
+ await vm.PruneCurrentGamePresetEntriesAsync();
+
+ if (isCounterStrikeFamilyGame)
+ {
+ if (!await vm.PruneCounterStrikeFamilyPresetEntriesAsync())
+ {
+ return;
+ }
+ }
await _jsonSetting.SetRevisionByGameModeAsync(fetchedRevision);
}
From ef096e810a461c73fcb584939f69a2aa48fd7655 Mon Sep 17 00:00:00 2001
From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com>
Date: Tue, 31 Mar 2026 23:09:28 -0400
Subject: [PATCH 04/10] refactor: move value equality to ServerPresetModel
---
ServerPickerX/Models/ServerPresetModel.cs | 16 ++++++++++++++++
ServerPickerX/Views/MainWindow.axaml.cs | 3 +--
2 files changed, 17 insertions(+), 2 deletions(-)
diff --git a/ServerPickerX/Models/ServerPresetModel.cs b/ServerPickerX/Models/ServerPresetModel.cs
index 1fb2070..8e73bdd 100644
--- a/ServerPickerX/Models/ServerPresetModel.cs
+++ b/ServerPickerX/Models/ServerPresetModel.cs
@@ -1,3 +1,4 @@
+using System;
using System.Collections.Generic;
namespace ServerPickerX.Models
@@ -11,5 +12,20 @@ public class ServerPresetModel
public bool IsClustered { get; set; }
public List BlockedServerKeys { get; set; } = [];
+
+ public override bool Equals(object? obj)
+ {
+ return obj is ServerPresetModel other &&
+ GameMode.Equals(other.GameMode, StringComparison.OrdinalIgnoreCase) &&
+ Name.Equals(other.Name, StringComparison.OrdinalIgnoreCase);
+ }
+
+ public override int GetHashCode()
+ {
+ return HashCode.Combine(
+ StringComparer.OrdinalIgnoreCase.GetHashCode(GameMode),
+ StringComparer.OrdinalIgnoreCase.GetHashCode(Name)
+ );
+ }
}
}
diff --git a/ServerPickerX/Views/MainWindow.axaml.cs b/ServerPickerX/Views/MainWindow.axaml.cs
index 68ce0c8..7a12fff 100644
--- a/ServerPickerX/Views/MainWindow.axaml.cs
+++ b/ServerPickerX/Views/MainWindow.axaml.cs
@@ -441,8 +441,7 @@ private static bool AreSamePresetSelection(ServerPresetModel? left, ServerPreset
return left == null && right == null;
}
- return left.GameMode.Equals(right.GameMode, StringComparison.OrdinalIgnoreCase) &&
- left.Name.Equals(right.Name, StringComparison.OrdinalIgnoreCase);
+ return left.Equals(right);
}
private void RefreshClusterButtonContent()
From f21d0d75bf414712a229719750524a8c92ee3ef5 Mon Sep 17 00:00:00 2001
From: JanitorialMess <65749353+JanitorialMess@users.noreply.github.com>
Date: Fri, 3 Apr 2026 06:13:24 -0400
Subject: [PATCH 05/10] refactor: move preset management to a separate window
---
.../Comparers/NaturalStringComparer.cs | 110 ++++
ServerPickerX/Locales/Locale_de-de.axaml | 30 +-
ServerPickerX/Locales/Locale_en-us.axaml | 28 +-
ServerPickerX/Locales/Locale_es-es.axaml | 30 +-
ServerPickerX/Locales/Locale_ja-jp.axaml | 28 +-
ServerPickerX/Locales/Locale_pl-pl.axaml | 28 +-
ServerPickerX/Locales/Locale_ru-ru.axaml | 28 +-
ServerPickerX/Locales/Locale_sv-se.axaml | 28 +-
ServerPickerX/Locales/Locale_tr-tr.axaml | 28 +-
ServerPickerX/Locales/Locale_zh-cn.axaml | 28 +-
.../Services/Settings/JsonSetting.cs | 1 -
.../ViewModels/MainWindowViewModel.cs | 65 +-
.../PresetManagerWindowViewModel.cs | 602 ++++++++++++++++++
ServerPickerX/Views/MainWindow.axaml | 45 +-
ServerPickerX/Views/MainWindow.axaml.cs | 87 +--
.../UserWindows/PresetManagerWindow.axaml | 262 ++++++++
.../UserWindows/PresetManagerWindow.axaml.cs | 382 +++++++++++
.../Views/UserWindows/PresetNameWindow.axaml | 75 ---
.../UserWindows/PresetNameWindow.axaml.cs | 45 --
19 files changed, 1567 insertions(+), 363 deletions(-)
create mode 100644 ServerPickerX/Comparers/NaturalStringComparer.cs
create mode 100644 ServerPickerX/ViewModels/PresetManagerWindowViewModel.cs
create mode 100644 ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml
create mode 100644 ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml.cs
delete mode 100644 ServerPickerX/Views/UserWindows/PresetNameWindow.axaml
delete mode 100644 ServerPickerX/Views/UserWindows/PresetNameWindow.axaml.cs
diff --git a/ServerPickerX/Comparers/NaturalStringComparer.cs b/ServerPickerX/Comparers/NaturalStringComparer.cs
new file mode 100644
index 0000000..be68255
--- /dev/null
+++ b/ServerPickerX/Comparers/NaturalStringComparer.cs
@@ -0,0 +1,110 @@
+using System;
+using System.Collections.Generic;
+
+namespace ServerPickerX.Comparers
+{
+ public sealed class NaturalStringComparer : IComparer
+ {
+ public static NaturalStringComparer OrdinalIgnoreCase { get; } = new(StringComparison.OrdinalIgnoreCase);
+
+ private readonly StringComparison _stringComparison;
+
+ public NaturalStringComparer(StringComparison stringComparison)
+ {
+ _stringComparison = stringComparison;
+ }
+
+ public int Compare(string? left, string? right)
+ {
+ if (ReferenceEquals(left, right))
+ {
+ return 0;
+ }
+
+ if (left == null)
+ {
+ return -1;
+ }
+
+ if (right == null)
+ {
+ return 1;
+ }
+
+ int leftIndex = 0;
+ int rightIndex = 0;
+
+ while (leftIndex < left.Length && rightIndex < right.Length)
+ {
+ if (char.IsDigit(left[leftIndex]) && char.IsDigit(right[rightIndex]))
+ {
+ int leftNumberStart = leftIndex;
+ int rightNumberStart = rightIndex;
+
+ while (leftIndex < left.Length && char.IsDigit(left[leftIndex]))
+ {
+ leftIndex++;
+ }
+
+ while (rightIndex < right.Length && char.IsDigit(right[rightIndex]))
+ {
+ rightIndex++;
+ }
+
+ ReadOnlySpan leftDigits = left.AsSpan(leftNumberStart, leftIndex - leftNumberStart);
+ ReadOnlySpan rightDigits = right.AsSpan(rightNumberStart, rightIndex - rightNumberStart);
+
+ int leftTrimmedStart = 0;
+ while (leftTrimmedStart < leftDigits.Length - 1 && leftDigits[leftTrimmedStart] == '0')
+ {
+ leftTrimmedStart++;
+ }
+
+ int rightTrimmedStart = 0;
+ while (rightTrimmedStart < rightDigits.Length - 1 && rightDigits[rightTrimmedStart] == '0')
+ {
+ rightTrimmedStart++;
+ }
+
+ ReadOnlySpan leftTrimmedDigits = leftDigits[leftTrimmedStart..];
+ ReadOnlySpan rightTrimmedDigits = rightDigits[rightTrimmedStart..];
+
+ if (leftTrimmedDigits.Length != rightTrimmedDigits.Length)
+ {
+ return leftTrimmedDigits.Length.CompareTo(rightTrimmedDigits.Length);
+ }
+
+ int digitComparison = leftTrimmedDigits.CompareTo(rightTrimmedDigits, StringComparison.Ordinal);
+
+ if (digitComparison != 0)
+ {
+ return digitComparison;
+ }
+
+ if (leftDigits.Length != rightDigits.Length)
+ {
+ return leftDigits.Length.CompareTo(rightDigits.Length);
+ }
+
+ continue;
+ }
+
+ int characterComparison = string.Compare(
+ left[leftIndex].ToString(),
+ right[rightIndex].ToString(),
+ _stringComparison
+ );
+
+ if (characterComparison != 0)
+ {
+ return characterComparison;
+ }
+
+ leftIndex++;
+ rightIndex++;
+ }
+
+ return left.Length.CompareTo(right.Length);
+ }
+ }
+}
diff --git a/ServerPickerX/Locales/Locale_de-de.axaml b/ServerPickerX/Locales/Locale_de-de.axaml
index ee7b083..a09e274 100644
--- a/ServerPickerX/Locales/Locale_de-de.axaml
+++ b/ServerPickerX/Locales/Locale_de-de.axaml
@@ -1,4 +1,4 @@
-
de-de
Server gruppieren
@@ -10,8 +10,6 @@
Ausgewählte blockieren
Alle freigeben
Ausgewählte freigeben
- Preset speichern
- Preset löschen
Speichern
Abbrechen
Bei Start nach neuen Versionen suchen
@@ -20,17 +18,9 @@
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 gerade von Valve aktualisiert! Alle blockierten Server werden entsperrt, um die neuen Serverdaten zu synchronisieren. Preset-Einträge für Server, die nicht mehr existieren, werden entfernt, und das zuletzt ausgewählte Preset wird erneut angewendet, falls verfügbar.
Neue Version verfügbar! Zu den Veröffentlichungen?
@@ -39,4 +29,22 @@
Moment! Ein Vorgang läuft bereits. Bitte warten Sie...
Dadurch wird versucht, die Firewall auf den Standardzustand zurückzusetzen. Aktion bestätigen?
Firewall erfolgreich zurückgesetzt!
+ Voreinstellungen
+ Voreinstellungen verwalten
+ Hinzufügen
+ Neues Preset
+ Neues Preset erstellen
+ Löschen
+ Ausgewählte Presets löschen
+ Anwenden
+ Ausgewähltes Preset anwenden
+ Voreinstellungen
+ Blockierte Server
+ Blockiert
+ Voreinstellung
+ Preset '{0}' löschen?
+ Die {0} ausgewählten Presets löschen?
+ Ein Preset mit dem Namen '{0}' existiert für dieses Spiel bereits. Überschreiben?
+ Das Wechseln zwischen gruppierter und entgruppierter Ansicht löscht die blockierten Einträge dieses Presets. Fortfahren?
+ Der Preset-Name darf nicht leer sein.
diff --git a/ServerPickerX/Locales/Locale_en-us.axaml b/ServerPickerX/Locales/Locale_en-us.axaml
index 333fe78..4ecc99f 100644
--- a/ServerPickerX/Locales/Locale_en-us.axaml
+++ b/ServerPickerX/Locales/Locale_en-us.axaml
@@ -10,8 +10,6 @@
Block Selected
Unblock All
Unblock Selected
- Save Preset
- Delete Preset
Save
Cancel
Check for new version on startup
@@ -20,17 +18,9 @@
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. Preset entries for servers that no longer exist will be removed, and the last selected preset will be reapplied if available.
New version available! Go to releases?
@@ -39,4 +29,22 @@
Whoa! There's already a pending operation. Please wait...
This will attempt to reset firewall to its default state. Confirm action?
Firewall has been successfully reset!
+ Presets
+ Manage presets
+ Add
+ New Preset
+ Create a new preset
+ Delete
+ Delete selected presets
+ Apply
+ Apply the selected preset
+ Presets
+ Blocked Servers
+ Blocked
+ Preset
+ Delete the preset '{0}'?
+ Delete the {0} selected presets?
+ A preset named '{0}' already exists for this game. Overwrite it?
+ Changing between clustered and unclustered view will clear this preset's blocked entries. Continue?
+ Preset name cannot be empty.
diff --git a/ServerPickerX/Locales/Locale_es-es.axaml b/ServerPickerX/Locales/Locale_es-es.axaml
index bd4c311..ed19545 100644
--- a/ServerPickerX/Locales/Locale_es-es.axaml
+++ b/ServerPickerX/Locales/Locale_es-es.axaml
@@ -1,4 +1,4 @@
-
es-es
Servidores de clúster
@@ -10,8 +10,6 @@
Bloque Seleccionado
Desbloquear Todo
Desbloquear Seleccionado
- Guardar preset
- Eliminar preset
Guardar
Cancelar
Comprobar si hay nueva versión al iniciar
@@ -20,17 +18,9 @@
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 se desbloquearán para sincronizar los nuevos datos. Se eliminarán las entradas del preset correspondientes a servidores que ya no existen y, si está disponible, se volverá a aplicar el último preset seleccionado.
¡Nueva versión disponible! ¿Ir a lanzamientos?
@@ -39,4 +29,22 @@
¡Vaya! Ya hay una operación pendiente. Por favor, espere...
Esto intentará restablecer el cortafuegos a su estado predeterminado. ¿Confirma la acción?
¡El cortafuegos se ha restablecido correctamente a su estado predeterminado!
+ Preajustes
+ Administrar preajustes
+ Añadir
+ Nuevo preajuste
+ Crear un nuevo preajuste
+ Eliminar
+ Eliminar los preajustes seleccionados
+ Aplicar
+ Aplicar el preajuste seleccionado
+ Preajustes
+ Servidores bloqueados
+ Bloqueado
+ Preajuste
+ ¿Eliminar el preset '{0}'?
+ ¿Eliminar los {0} preajustes seleccionados?
+ Ya existe un preset llamado '{0}' para este juego. ¿Sobrescribirlo?
+ Cambiar entre la vista agrupada y no agrupada borrará las entradas bloqueadas de este preajuste. ¿Continuar?
+ El nombre del preset no puede estar vacío.
diff --git a/ServerPickerX/Locales/Locale_ja-jp.axaml b/ServerPickerX/Locales/Locale_ja-jp.axaml
index 8c7d27b..82aef1f 100644
--- a/ServerPickerX/Locales/Locale_ja-jp.axaml
+++ b/ServerPickerX/Locales/Locale_ja-jp.axaml
@@ -10,8 +10,6 @@
選択したサーバーをブロック
すべて解除
選択したサーバーを解除
- プリセットを保存
- プリセットを削除
保存
キャンセル
起動時に新しいバージョンを確認
@@ -20,17 +18,9 @@
プリセットを選択
サーバーをグループ化またはグループ解除
すべてのサーバーの ping を更新
- 現在ブロック中のサーバーをプリセットとして保存
- 選択したプリセットを削除
この操作は、ファイアウォールの競合を防ぐためにまずすべてのサーバーを解除します。
- プリセット '{0}' を削除しますか?
- このゲームには '{0}' という名前のプリセットが既にあります。上書きしますか?
- プリセット名は空にできません。
言語を選択
ファイアウォールルールをリセット
- プリセットを保存
- 現在のゲーム用のプリセット名を入力してください。
- プリセット名
情報
Valve によってサーバーデータが更新されました。新しいサーバーデータと同期するために、ブロックされていたすべてのサーバーのブロックが解除されます。存在しなくなったサーバーのプリセット項目は削除され、可能であれば最後に選択したプリセットが再適用されます。
新バージョンがリリースされました!リリース一覧へどうぞ。
@@ -39,4 +29,22 @@
おっと!既に処理が実行されています。しばらくお待ちください。
これにより、ファイアウォールがデフォルトの状態にリセットされます。操作を確定しますか?
ファイアウォールが正常にリセットされました!
+ プリセット
+ プリセットの管理
+ 追加
+ 新しいプリセット
+ 新しいプリセットを作成
+ 削除
+ 選択したプリセットを削除
+ 適用
+ 選択したプリセットを適用
+ プリセット
+ ブロック中のサーバー
+ ブロック
+ プリセット
+ プリセット '{0}' を削除しますか?
+ 選択した {0} 個のプリセットを削除しますか?
+ このゲームには '{0}' という名前のプリセットが既にあります。上書きしますか?
+ クラスタ表示と非クラスタ表示を切り替えると、このプリセットのブロック済み項目は消去されます。続行しますか?
+ プリセット名は空にできません。
diff --git a/ServerPickerX/Locales/Locale_pl-pl.axaml b/ServerPickerX/Locales/Locale_pl-pl.axaml
index 37129ab..edf2f13 100644
--- a/ServerPickerX/Locales/Locale_pl-pl.axaml
+++ b/ServerPickerX/Locales/Locale_pl-pl.axaml
@@ -10,8 +10,6 @@
Blok wybrany
Odblokuj wszystko
Odblokuj wybrane
- Zapisz preset
- Usuń preset
Zapisz
Anuluj
Sprawdź nową wersję podczas uruchamiania
@@ -20,17 +18,9 @@
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 serwerów. Wszystkie zablokowane serwery zostaną odblokowane, aby zsynchronizować nowe dane serwera. Wpisy presetów dla serwerów, które już nie istnieją, zostaną usunięte, a ostatnio wybrany preset zostanie zastosowany ponownie, jeśli jest dostępny.
Nowa wersja dostępna! Przejdź do wydań?
@@ -39,4 +29,22 @@
Ojej! Jest już operacja w toku. Proszę czekać...
Spowoduje to próbę przywrócenia zapory do stanu domyślnego. Czy potwierdzić działanie?
Zapora sieciowa została pomyślnie zresetowana!
+ Presety
+ Zarządzaj presetami
+ Dodaj
+ Nowy preset
+ Utwórz nowy preset
+ Usuń
+ Usuń wybrane presety
+ Zastosuj
+ Zastosuj wybrany preset
+ Presety
+ Zablokowane serwery
+ Zablokowany
+ Preset
+ Usunąć preset '{0}'?
+ Usunąć {0} wybranych presetów?
+ Preset o nazwie '{0}' już istnieje dla tej gry. Nadpisać?
+ Przełączenie między widokiem grupowanym i niegrupowanym wyczyści zablokowane wpisy tego presetu. Kontynuować?
+ Nazwa presetu nie może być pusta.
diff --git a/ServerPickerX/Locales/Locale_ru-ru.axaml b/ServerPickerX/Locales/Locale_ru-ru.axaml
index 3298ec9..4a2349c 100644
--- a/ServerPickerX/Locales/Locale_ru-ru.axaml
+++ b/ServerPickerX/Locales/Locale_ru-ru.axaml
@@ -10,8 +10,6 @@
Блокировать выбранные
Разблокировать все
Разблокировать выбранные
- Сохранить пресет
- Удалить пресет
Сохранить
Отмена
Проверять новые версии при запуске
@@ -20,17 +18,9 @@
Выбрать пресет
Группировать или разгруппировать серверы
Обновить пинг всех серверов
- Сохранить текущие заблокированные серверы как пресет
- Удалить выбранный пресет
Это действие сначала разблокирует все сервера для предотвращения конфликтов брандмауэра.
- Удалить пресет '{0}'?
- Пресет с именем '{0}' уже существует для этой игры. Перезаписать его?
- Имя пресета не может быть пустым.
Выберите язык
Сбросить правила брандмауэра
- Сохранить пресет
- Введите имя пресета для текущей игры.
- Имя пресета
информация
Valve обновила данные серверов. Все заблокированные серверы будут разблокированы для синхронизации новых данных. Записи пресета для серверов, которые больше не существуют, будут удалены, а последний выбранный пресет будет применен снова, если он доступен.
Доступна новая версия! Перейти к релизам?
@@ -39,4 +29,22 @@
Ого! Уже есть ожидающая операция. Пожалуйста, подождите...
Это попытается сбросить брандмауэр до состояния по умолчанию. Подтвердите действие?
Межсетевой экран успешно перезагружен и вернулся в состояние по умолчанию!
+ Пресеты
+ Управление пресетами
+ Добавить
+ Новый пресет
+ Создать новый пресет
+ Удалить
+ Удалить выбранные пресеты
+ Применить
+ Применить выбранный пресет
+ Пресеты
+ Заблокированные серверы
+ Блок
+ Пресет
+ Удалить пресет '{0}'?
+ Удалить {0} выбранных пресетов?
+ Пресет с именем '{0}' уже существует для этой игры. Перезаписать его?
+ Переключение между сгруппированным и несгруппированным видом очистит заблокированные записи этого пресета. Продолжить?
+ Имя пресета не может быть пустым.
diff --git a/ServerPickerX/Locales/Locale_sv-se.axaml b/ServerPickerX/Locales/Locale_sv-se.axaml
index 3ec510c..aee60b9 100644
--- a/ServerPickerX/Locales/Locale_sv-se.axaml
+++ b/ServerPickerX/Locales/Locale_sv-se.axaml
@@ -10,8 +10,6 @@
Blockera valda
Avblockera alla
Avblockera valda
- Spara förinställning
- Ta bort förinställning
Spara
Avbryt
Kontrollera efter ny version vid start
@@ -20,17 +18,9 @@
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 just uppdaterats av Valve. Alla blockerade servrar kommer att avblockeras för att synkronisera ny serverdata. Förinställningsposter för servrar som inte längre finns kommer att tas bort, och den senast valda förinställningen kommer att tillämpas igen om den fortfarande finns.
Ny version tillgänglig! Gå till utgåvor?
@@ -39,4 +29,22 @@
Oj! Det finns redan en väntande åtgärd. Vänta...
Detta kommer att försöka återställa brandväggen till standardläget. Bekräfta åtgärd?
Brandväggen har återställts!
+ Förinställningar
+ Hantera förinställningar
+ Lägg till
+ Ny förinställning
+ Skapa en ny förinställning
+ Ta bort
+ Ta bort markerade förinställningar
+ Verkställ
+ Verkställ vald förinställning
+ Förinställningar
+ Blockerade servrar
+ Blockerad
+ Förinställning
+ Ta bort förinställningen '{0}'?
+ Ta bort de {0} valda förinställningarna?
+ En förinställning med namnet '{0}' finns redan för det här spelet. Skriva över den?
+ Att växla mellan grupperad och ogrupperad vy kommer att rensa det här förinställets blockerade poster. Fortsätta?
+ Namnet på förinställningen får inte vara tomt.
diff --git a/ServerPickerX/Locales/Locale_tr-tr.axaml b/ServerPickerX/Locales/Locale_tr-tr.axaml
index 619faa1..e5962ce 100644
--- a/ServerPickerX/Locales/Locale_tr-tr.axaml
+++ b/ServerPickerX/Locales/Locale_tr-tr.axaml
@@ -10,8 +10,6 @@
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
@@ -20,17 +18,9 @@
Ö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. Artık mevcut olmayan sunuculara ait preset girdileri kaldırılacak ve varsa son seçilen preset yeniden uygulanacak.
Yeni sürüm yayınlandı! Güncelle?
@@ -39,4 +29,22 @@
Zaten bekleyen bir işlem var. Lütfen bekleyin...
Bu işlem güvenlik duvarını sıfırlayacak. Onaylıyor musun?
Güvenlik duvarı başarıyla sıfırlandı!
+ Ön Ayarlar
+ Ön ayarları yönet
+ Ekle
+ Yeni ön ayar
+ Yeni bir ön ayar oluştur
+ Sil
+ Seçili ön ayarları sil
+ Uygula
+ Seçili ön ayarı uygula
+ Ön Ayarlar
+ Engellenen sunucular
+ Engelli
+ Ön Ayar
+ '{0}' ön ayarı silinsin mi?
+ Seçili {0} ön ayar silinsin mi?
+ Bu oyun için '{0}' adında bir ön ayar zaten var. Üzerine yazılsın mı?
+ Kümelenmiş ve kümelenmemiş görünüm arasında geçiş yapmak bu ön ayarın engellenen girdilerini temizleyecek. Devam edilsin mi?
+ Ön ayar adı boş olamaz.
diff --git a/ServerPickerX/Locales/Locale_zh-cn.axaml b/ServerPickerX/Locales/Locale_zh-cn.axaml
index 40d3bb8..1a35430 100644
--- a/ServerPickerX/Locales/Locale_zh-cn.axaml
+++ b/ServerPickerX/Locales/Locale_zh-cn.axaml
@@ -10,8 +10,6 @@
块选定
全部解锁
取消阻止所选内容
- 保存预设
- 删除预设
保存
取消
启动时检查新版本
@@ -20,17 +18,9 @@
选择预设
分组或取消分组服务器
刷新所有服务器 ping
- 将当前已屏蔽的服务器保存为预设
- 删除所选预设
此操作将首先解除所有服务器的阻止,以防止防火墙冲突。
- 要删除预设“{0}”吗?
- 此游戏已存在名为“{0}”的预设。要覆盖吗?
- 预设名称不能为空。
选择语言
重置防火墙规则
- 保存预设
- 为当前游戏输入一个预设名称。
- 预设名称
信息
Valve 刚刚更新了服务器数据。所有被屏蔽的服务器都将被解除屏蔽,以便同步新的服务器数据。对于已不存在的服务器,其预设条目将被移除,并且如果可用,最后选中的预设将被重新应用。
新版本已发布!前往发布页面?
@@ -39,4 +29,22 @@
目前有待处理的操作。请稍候……
此操作将尝试把防火墙重置为默认状态。确认操作?
防火墙已成功重置!
+ 预设
+ 管理预设
+ 添加
+ 新预设
+ 创建新预设
+ 删除
+ 删除所选预设
+ 应用
+ 应用所选预设
+ 预设
+ 已屏蔽服务器
+ 已屏蔽
+ 预设
+ 要删除预设“{0}”吗?
+ 要删除这 {0} 个选中的预设吗?
+ 此游戏已存在名为“{0}”的预设。要覆盖吗?
+ 在集群视图和非集群视图之间切换将清除此预设中已阻止的条目。是否继续?
+ 预设名称不能为空。
diff --git a/ServerPickerX/Services/Settings/JsonSetting.cs b/ServerPickerX/Services/Settings/JsonSetting.cs
index 9baedae..b8ec223 100644
--- a/ServerPickerX/Services/Settings/JsonSetting.cs
+++ b/ServerPickerX/Services/Settings/JsonSetting.cs
@@ -245,7 +245,6 @@ public List GetPresetsByGameMode(string gameMode)
return (server_presets ?? [])
.Where(preset => (preset.GameMode ?? string.Empty).Equals(normalizedGameMode, StringComparison.OrdinalIgnoreCase))
- .OrderBy(preset => preset.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
diff --git a/ServerPickerX/ViewModels/MainWindowViewModel.cs b/ServerPickerX/ViewModels/MainWindowViewModel.cs
index 2fcff85..23ed595 100644
--- a/ServerPickerX/ViewModels/MainWindowViewModel.cs
+++ b/ServerPickerX/ViewModels/MainWindowViewModel.cs
@@ -51,18 +51,15 @@ 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]
@@ -72,8 +69,6 @@ public partial class MainWindowViewModel : ViewModelBase
// 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;
@@ -225,44 +220,20 @@ public void SelectPresetByName(string presetName)
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);
+ public string GetCurrentGameMode() => _jsonSetting.game_mode;
- _presetNameSuggestion = serverPreset.Name;
- LoadPresetPickerItems();
- SelectPresetByName(serverPreset.Name);
- }
-
- public string GetPresetNameSuggestion()
+ public IReadOnlyList GetCurrentGameServerModels(bool isClustered)
{
- return SelectedPreset?.Name ?? _presetNameSuggestion;
- }
+ ServerData serverData = _serverDataService.GetServerData();
- public bool IsSuggestedPresetName(string presetName)
- {
- return GetPresetNameSuggestion().Equals(presetName, StringComparison.OrdinalIgnoreCase);
+ return isClustered
+ ? serverData.ClusteredServers
+ : serverData.UnclusteredServers;
}
- public async Task DeleteSelectedPresetAsync()
+ public async Task DeletePresetAsync(ServerPresetModel preset)
{
- if (SelectedPreset == null)
- {
- return;
- }
-
- string deletedPresetName = SelectedPreset.Name;
+ string deletedPresetName = preset.Name;
await _jsonSetting.RemovePresetAsync(_jsonSetting.game_mode, deletedPresetName);
@@ -271,13 +242,18 @@ public async Task DeleteSelectedPresetAsync()
await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync();
}
- if (GetPresetNameSuggestion().Equals(deletedPresetName, StringComparison.OrdinalIgnoreCase))
+ if ((SelectedPreset?.Name ?? string.Empty).Equals(deletedPresetName, StringComparison.OrdinalIgnoreCase) ||
+ _presetNameSuggestion.Equals(deletedPresetName, StringComparison.OrdinalIgnoreCase))
{
_presetNameSuggestion = string.Empty;
}
LoadPresetPickerItems();
- ClearSelectedPreset();
+
+ if (SelectedPreset?.Equals(preset) == true)
+ {
+ ClearSelectedPreset();
+ }
}
public async Task ApplyPresetAsync(ServerPresetModel serverPreset)
@@ -563,13 +539,20 @@ private void TrackBlockedServerKeys(IEnumerable serverModels, bool
}
}
- private string GetServerKey(ServerModel serverModel)
+ public string GetCurrentServerKey(ServerModel serverModel) => GetServerKey(serverModel);
+
+ public string GetServerKey(ServerModel serverModel, bool isClustered)
{
- return _jsonSetting.is_clustered
+ return isClustered
? serverModel.Description
: serverModel.Name;
}
+ private string GetServerKey(ServerModel serverModel)
+ {
+ return GetServerKey(serverModel, _jsonSetting.is_clustered);
+ }
+
public async Task RestoreLastSelectedPresetAsync()
{
if (!HasPresets)
diff --git a/ServerPickerX/ViewModels/PresetManagerWindowViewModel.cs b/ServerPickerX/ViewModels/PresetManagerWindowViewModel.cs
new file mode 100644
index 0000000..bfab235
--- /dev/null
+++ b/ServerPickerX/ViewModels/PresetManagerWindowViewModel.cs
@@ -0,0 +1,602 @@
+using CommunityToolkit.Mvvm.ComponentModel;
+using MsBox.Avalonia.Enums;
+using Avalonia.Platform;
+using ServerPickerX.Comparers;
+using ServerPickerX.Extensions;
+using ServerPickerX.Models;
+using ServerPickerX.Services.Localizations;
+using ServerPickerX.Services.MessageBoxes;
+using ServerPickerX.Settings;
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Security.Cryptography;
+using System.Threading.Tasks;
+
+namespace ServerPickerX.ViewModels
+{
+ public partial class PresetManagerWindowViewModel : ViewModelBase
+ {
+ [ObservableProperty]
+ private ObservableCollectionExtended presets = [];
+
+ public ObservableCollectionExtended ServerItems { get; } = [];
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(CanDelete))]
+ [NotifyPropertyChangedFor(nameof(CanApply))]
+ [NotifyPropertyChangedFor(nameof(CanEditPreset))]
+ [NotifyPropertyChangedFor(nameof(CanToggleClusterMode))]
+ private PresetListItemViewModel? selectedPresetItem;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(CanDelete))]
+ private bool isDeletingPreset;
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(ClusterToggleText))]
+ private bool editorIsClustered;
+
+ private readonly MainWindowViewModel _mainVm;
+ private readonly JsonSetting _jsonSetting;
+ private readonly IMessageBoxService _messageBoxService;
+ private readonly ILocalizationService _localizationService;
+
+ public string WindowTitle =>
+ $"{_localizationService.GetLocaleValue("PresetsWindowTitle")} - {_mainVm.GetCurrentGameMode()}";
+
+ public bool CanDelete => SelectedPresetItem != null && !IsDeletingPreset;
+
+ public bool CanApply => SelectedPresetItem != null;
+
+ public bool CanEditPreset => SelectedPresetItem != null;
+
+ public bool CanToggleClusterMode => SelectedPresetItem != null;
+
+ public string ClusterToggleText => _localizationService.GetLocaleValue(
+ EditorIsClustered ? "UnclusterServers" : "ClusterServers");
+
+ public PresetManagerWindowViewModel(
+ MainWindowViewModel mainVm,
+ JsonSetting jsonSetting,
+ IMessageBoxService messageBoxService,
+ ILocalizationService localizationService
+ )
+ {
+ _mainVm = mainVm;
+ _jsonSetting = jsonSetting;
+ _messageBoxService = messageBoxService;
+ _localizationService = localizationService;
+ EditorIsClustered = false;
+
+ ReloadPresets(_mainVm.SelectedPreset?.Name);
+ }
+
+ partial void OnSelectedPresetItemChanged(PresetListItemViewModel? oldValue, PresetListItemViewModel? newValue)
+ {
+ if (oldValue != null && !ReferenceEquals(oldValue, newValue))
+ {
+ oldValue.IsEditing = false;
+ }
+
+ if (newValue != null)
+ {
+ EditorIsClustered = newValue.Preset.IsClustered;
+ }
+ else
+ {
+ EditorIsClustered = false;
+ }
+
+ LoadServerItemsForSelectedPreset();
+ }
+
+ public async Task AddPresetAsync()
+ {
+ string presetName = GetNextPresetName();
+ ServerPresetModel newPreset = new()
+ {
+ Name = presetName,
+ GameMode = _mainVm.GetCurrentGameMode(),
+ IsClustered = false,
+ BlockedServerKeys = [],
+ };
+
+ await _jsonSetting.AddOrUpdatePresetAsync(ClonePreset(newPreset));
+
+ ReloadPresets(presetName);
+ }
+
+ public async Task DeletePresetsAsync(IReadOnlyList presetsToDelete)
+ {
+ if (presetsToDelete == null || presetsToDelete.Count == 0 || IsDeletingPreset)
+ {
+ return false;
+ }
+
+ List normalizedPresets = presetsToDelete
+ .Where(preset => preset != null)
+ .Distinct()
+ .ToList();
+
+ if (normalizedPresets.Count == 0)
+ {
+ return false;
+ }
+
+ IsDeletingPreset = true;
+
+ try
+ {
+ string confirmationText = normalizedPresets.Count == 1
+ ? string.Format(
+ _localizationService.GetLocaleValue("PresetDeleteConfirmDialogue"),
+ normalizedPresets[0].Name
+ )
+ : string.Format(
+ _localizationService.GetLocaleValue("PresetDeleteSelectedConfirmDialogue"),
+ normalizedPresets.Count
+ );
+
+ bool shouldDelete = await _messageBoxService.ShowMessageBoxConfirmationAsync(
+ _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
+ confirmationText,
+ Icon.Setting
+ );
+
+ if (!shouldDelete)
+ {
+ return false;
+ }
+
+ List currentPresets = GetCurrentGamePresets();
+ HashSet deletedPresetNames = normalizedPresets
+ .Select(preset => preset.Name)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+ string? preferredPresetName = currentPresets
+ .FirstOrDefault(preset => !deletedPresetNames.Contains(preset.Name))
+ ?.Name;
+
+ foreach (ServerPresetModel preset in normalizedPresets)
+ {
+ await _mainVm.DeletePresetAsync(preset);
+ }
+
+ ReloadPresets(preferredPresetName);
+
+ return true;
+ }
+ finally
+ {
+ IsDeletingPreset = false;
+ }
+ }
+
+ public async Task ApplySelectedPresetAsync()
+ {
+ if (SelectedPresetItem == null)
+ {
+ return false;
+ }
+
+ // The modal can add/delete presets without touching the main dropdown state.
+ // Refresh the main preset list before applying so the selected preset can be reselected there too.
+ _mainVm.LoadPresetPickerItems();
+
+ return await _mainVm.ApplyPresetAsync(ClonePreset(SelectedPresetItem.Preset));
+ }
+
+ public async Task RenamePresetAsync(PresetListItemViewModel presetItem, string originalPresetName)
+ {
+ if (presetItem == null)
+ {
+ return false;
+ }
+
+ string newPresetName = (presetItem.Name ?? string.Empty).Trim();
+ string currentPresetName = originalPresetName.Trim();
+
+ if (string.IsNullOrWhiteSpace(newPresetName))
+ {
+ await _messageBoxService.ShowMessageBoxAsync(
+ _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
+ _localizationService.GetLocaleValue("PresetNameRequiredDialogue")
+ );
+
+ ReloadPresets(currentPresetName);
+ return false;
+ }
+
+ if (newPresetName.Equals(currentPresetName, StringComparison.OrdinalIgnoreCase))
+ {
+ ReloadPresets(currentPresetName);
+ return true;
+ }
+
+ ServerPresetModel? existingPreset = _mainVm.GetCurrentGamePreset(newPresetName);
+ bool overwritingDifferentPreset = existingPreset != null &&
+ !existingPreset.Name.Equals(currentPresetName, StringComparison.OrdinalIgnoreCase);
+
+ if (overwritingDifferentPreset)
+ {
+ bool shouldOverwrite = await _messageBoxService.ShowMessageBoxConfirmationAsync(
+ _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
+ string.Format(
+ _localizationService.GetLocaleValue("PresetOverwriteConfirmDialogue"),
+ newPresetName
+ ),
+ Icon.Setting
+ );
+
+ if (!shouldOverwrite)
+ {
+ ReloadPresets(currentPresetName);
+ return false;
+ }
+ }
+
+ ServerPresetModel renamedPreset = ClonePreset(presetItem.Preset);
+ renamedPreset.Name = newPresetName;
+
+ await _jsonSetting.AddOrUpdatePresetAsync(renamedPreset);
+
+ await _jsonSetting.RemovePresetAsync(_mainVm.GetCurrentGameMode(), currentPresetName);
+ await SyncPresetReferenceAfterRenameAsync(currentPresetName, newPresetName, overwritingDifferentPreset);
+
+ ReloadPresets(newPresetName);
+
+ return true;
+ }
+
+ public async Task PersistSelectedPresetServerKeysAsync()
+ {
+ if (SelectedPresetItem == null)
+ {
+ return;
+ }
+
+ SelectedPresetItem.Preset.BlockedServerKeys = ServerItems
+ .Where(serverItem => serverItem.IsBlocked)
+ .Select(serverItem => serverItem.Key)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(serverKey => serverKey, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ await _jsonSetting.AddOrUpdatePresetAsync(ClonePreset(SelectedPresetItem.Preset));
+ await ClearAppliedPresetReferenceIfNeededAsync(SelectedPresetItem.Preset.Name);
+ }
+
+ public async Task ToggleSelectedPresetClusterModeAsync()
+ {
+ if (SelectedPresetItem == null)
+ {
+ return;
+ }
+
+ bool hasBlockedEntries = (SelectedPresetItem.Preset.BlockedServerKeys?.Count ?? 0) > 0;
+
+ if (hasBlockedEntries)
+ {
+ bool shouldChangeMode = await _messageBoxService.ShowMessageBoxConfirmationAsync(
+ _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
+ _localizationService.GetLocaleValue("PresetChangeViewModeConfirmDialogue"),
+ Icon.Setting
+ );
+
+ if (!shouldChangeMode)
+ {
+ return;
+ }
+ }
+
+ SelectedPresetItem.Preset.IsClustered = !SelectedPresetItem.Preset.IsClustered;
+ SelectedPresetItem.Preset.BlockedServerKeys = [];
+ EditorIsClustered = SelectedPresetItem.Preset.IsClustered;
+
+ await _jsonSetting.AddOrUpdatePresetAsync(ClonePreset(SelectedPresetItem.Preset));
+ await ClearAppliedPresetReferenceIfNeededAsync(SelectedPresetItem.Preset.Name);
+
+ LoadServerItemsForSelectedPreset();
+ }
+
+ // Use NaturalStringComparer to sort preset names.
+ // This is to avoid issues with numbers in preset names being treated as part of the number sequence.
+ // For example, "Preset 1" should come before "Preset 10" in ascending order, not after.
+ public void SortPresets(ListSortDirection direction)
+ {
+ string? selectedPresetName = SelectedPresetItem?.Name;
+ List sortedPresets = (direction == ListSortDirection.Ascending
+ ? Presets.OrderBy(preset => preset.Name, NaturalStringComparer.OrdinalIgnoreCase)
+ : Presets.OrderByDescending(preset => preset.Name, NaturalStringComparer.OrdinalIgnoreCase))
+ .ToList();
+
+ Presets.Clear();
+ Presets.AddRange(sortedPresets);
+
+ if (!string.IsNullOrWhiteSpace(selectedPresetName))
+ {
+ SelectedPresetItem = Presets.FirstOrDefault(preset =>
+ preset.Name.Equals(selectedPresetName, StringComparison.OrdinalIgnoreCase));
+ }
+ }
+
+ public void SortServerItems(string sortKey, ListSortDirection direction)
+ {
+ List sortedItems = sortKey switch
+ {
+ "Blocked" => (direction == ListSortDirection.Ascending
+ ? ServerItems.OrderBy(serverItem => serverItem.IsBlocked)
+ : ServerItems.OrderByDescending(serverItem => serverItem.IsBlocked)).ToList(),
+ "Flag" => (direction == ListSortDirection.Ascending
+ ? ServerItems.OrderBy(serverItem => serverItem.FlagSortKey, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(serverItem => serverItem.Description, StringComparer.OrdinalIgnoreCase)
+ : ServerItems.OrderByDescending(serverItem => serverItem.FlagSortKey, StringComparer.OrdinalIgnoreCase)
+ .ThenBy(serverItem => serverItem.Description, StringComparer.OrdinalIgnoreCase)).ToList(),
+ "ServerId" => (direction == ListSortDirection.Ascending
+ ? ServerItems.OrderBy(serverItem => serverItem.Name, StringComparer.OrdinalIgnoreCase)
+ : ServerItems.OrderByDescending(serverItem => serverItem.Name, StringComparer.OrdinalIgnoreCase)).ToList(),
+ _ => (direction == ListSortDirection.Ascending
+ ? ServerItems.OrderBy(serverItem => serverItem.Description, StringComparer.OrdinalIgnoreCase)
+ : ServerItems.OrderByDescending(serverItem => serverItem.Description, StringComparer.OrdinalIgnoreCase)).ToList(),
+ };
+
+ ServerItems.Clear();
+ ServerItems.AddRange(sortedItems);
+ }
+
+ private void ReloadPresets(string? preferredPresetName = null)
+ {
+ string? presetNameToSelect = preferredPresetName ?? SelectedPresetItem?.Name;
+ List currentPresets = GetCurrentGamePresets();
+
+ if (currentPresets.Count == 0)
+ {
+ Presets = [];
+ SelectedPresetItem = null;
+ ServerItems.Clear();
+ return;
+ }
+
+ Presets = new ObservableCollectionExtended(
+ currentPresets.Select(preset => new PresetListItemViewModel(ClonePreset(preset))).ToList()
+ );
+
+ SelectedPresetItem = !string.IsNullOrWhiteSpace(presetNameToSelect)
+ ? Presets.FirstOrDefault(preset =>
+ preset.Name.Equals(presetNameToSelect, StringComparison.OrdinalIgnoreCase))
+ : null;
+
+ SelectedPresetItem ??= Presets[0];
+ }
+
+ private void LoadServerItemsForSelectedPreset()
+ {
+ ServerItems.Clear();
+
+ if (SelectedPresetItem == null)
+ {
+ return;
+ }
+
+ HashSet blockedServerKeys = (SelectedPresetItem.Preset.BlockedServerKeys ?? [])
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ foreach (ServerModel serverModel in _mainVm.GetCurrentGameServerModels(SelectedPresetItem.Preset.IsClustered))
+ {
+ string serverKey = _mainVm.GetServerKey(serverModel, SelectedPresetItem.Preset.IsClustered);
+
+ ServerItems.Add(new PresetServerSelectionItem(serverModel, serverKey, blockedServerKeys.Contains(serverKey)));
+ }
+ }
+
+ public void StopEditingPresets()
+ {
+ foreach (PresetListItemViewModel preset in Presets)
+ {
+ preset.IsEditing = false;
+ }
+ }
+
+ private List GetCurrentGamePresets()
+ {
+ return _jsonSetting.GetPresetsByGameMode(_mainVm.GetCurrentGameMode());
+ }
+
+ private async Task ClearAppliedPresetReferenceIfNeededAsync(string presetName)
+ {
+ bool matchesAppliedPreset = (_mainVm.SelectedPreset?.Name ?? string.Empty)
+ .Equals(presetName, StringComparison.OrdinalIgnoreCase);
+ bool matchesLastSelected = (_jsonSetting.GetLastSelectedPresetNameByGameMode() ?? string.Empty)
+ .Equals(presetName, StringComparison.OrdinalIgnoreCase);
+
+ if (!matchesAppliedPreset && !matchesLastSelected)
+ {
+ return;
+ }
+
+ if (matchesLastSelected)
+ {
+ await _jsonSetting.ClearLastSelectedPresetNameByGameModeAsync();
+ }
+
+ if (matchesAppliedPreset)
+ {
+ _mainVm.SelectPresetByName(string.Empty);
+ }
+ }
+
+ private async Task SyncPresetReferenceAfterRenameAsync(
+ string originalPresetName,
+ string renamedPresetName,
+ bool overwroteDifferentPreset
+ )
+ {
+ if (overwroteDifferentPreset)
+ {
+ bool targetWasAppliedOrRemembered =
+ (_mainVm.SelectedPreset?.Name ?? string.Empty).Equals(renamedPresetName, StringComparison.OrdinalIgnoreCase) ||
+ (_jsonSetting.GetLastSelectedPresetNameByGameMode() ?? string.Empty).Equals(renamedPresetName, StringComparison.OrdinalIgnoreCase);
+
+ if (targetWasAppliedOrRemembered)
+ {
+ await ClearAppliedPresetReferenceIfNeededAsync(renamedPresetName);
+ }
+
+ if ((_mainVm.SelectedPreset?.Name ?? string.Empty).Equals(originalPresetName, StringComparison.OrdinalIgnoreCase) ||
+ (_jsonSetting.GetLastSelectedPresetNameByGameMode() ?? string.Empty).Equals(originalPresetName, StringComparison.OrdinalIgnoreCase))
+ {
+ await ClearAppliedPresetReferenceIfNeededAsync(originalPresetName);
+ }
+
+ return;
+ }
+
+ bool renamedAppliedPreset = (_mainVm.SelectedPreset?.Name ?? string.Empty)
+ .Equals(originalPresetName, StringComparison.OrdinalIgnoreCase);
+ bool renamedLastSelected = (_jsonSetting.GetLastSelectedPresetNameByGameMode() ?? string.Empty)
+ .Equals(originalPresetName, StringComparison.OrdinalIgnoreCase);
+
+ if (renamedLastSelected)
+ {
+ await _jsonSetting.SetLastSelectedPresetNameByGameModeAsync(renamedPresetName);
+ }
+
+ if (renamedAppliedPreset)
+ {
+ _mainVm.LoadPresetPickerItems();
+ _mainVm.SelectPresetByName(renamedPresetName);
+ }
+ }
+
+ private string GetNextPresetName()
+ {
+ string basePresetName = _localizationService.GetLocaleValue("NewPresetName");
+ HashSet existingPresetNames = GetCurrentGamePresets()
+ .Select(preset => preset.Name)
+ .ToHashSet(StringComparer.OrdinalIgnoreCase);
+
+ if (!existingPresetNames.Contains(basePresetName))
+ {
+ return basePresetName;
+ }
+
+ for (int suffix = 2; ; suffix++)
+ {
+ string candidateName = $"{basePresetName} ({suffix})";
+
+ if (!existingPresetNames.Contains(candidateName))
+ {
+ return candidateName;
+ }
+ }
+ }
+
+ private static ServerPresetModel ClonePreset(ServerPresetModel preset)
+ {
+ return new ServerPresetModel
+ {
+ Name = preset.Name,
+ GameMode = preset.GameMode,
+ IsClustered = preset.IsClustered,
+ BlockedServerKeys = (preset.BlockedServerKeys ?? [])
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(serverKey => serverKey, StringComparer.OrdinalIgnoreCase)
+ .ToList(),
+ };
+ }
+ }
+
+ public partial class PresetListItemViewModel : ObservableObject
+ {
+ public ServerPresetModel Preset { get; }
+
+ public string Name
+ {
+ get => Preset.Name;
+ set
+ {
+ if (Preset.Name == value)
+ {
+ return;
+ }
+
+ Preset.Name = value;
+ OnPropertyChanged();
+ }
+ }
+
+ [ObservableProperty]
+ [NotifyPropertyChangedFor(nameof(IsDisplayVisible))]
+ [NotifyPropertyChangedFor(nameof(IsEditorVisible))]
+ private bool isEditing;
+
+ public bool IsDisplayVisible => !IsEditing;
+
+ public bool IsEditorVisible => IsEditing;
+
+ public PresetListItemViewModel(ServerPresetModel preset)
+ {
+ Preset = preset;
+ }
+ }
+
+ public partial class PresetServerSelectionItem : ObservableObject
+ {
+ private static readonly Dictionary FlagSortKeyCache = new(StringComparer.OrdinalIgnoreCase);
+
+ public ServerModel ServerModel { get; }
+
+ public string Key { get; }
+
+ public string Flag => ServerModel.Flag;
+
+ public string FlagSortKey { get; }
+
+ public string Name => ServerModel.Name;
+
+ public string Description => ServerModel.Description;
+
+ [ObservableProperty]
+ private bool isBlocked;
+
+ public PresetServerSelectionItem(ServerModel serverModel, string key, bool isBlocked)
+ {
+ ServerModel = serverModel;
+ Key = key;
+ IsBlocked = isBlocked;
+ FlagSortKey = GetFlagSortKey(serverModel.Flag);
+ }
+
+ private static string GetFlagSortKey(string flagPath)
+ {
+ if (string.IsNullOrWhiteSpace(flagPath))
+ {
+ return string.Empty;
+ }
+
+ if (FlagSortKeyCache.TryGetValue(flagPath, out string? cachedValue))
+ {
+ return cachedValue;
+ }
+
+ string assetUri = $"avares://ServerPickerX{flagPath}";
+
+ try
+ {
+ using var stream = AssetLoader.Open(new Uri(assetUri));
+ byte[] hash = SHA256.HashData(stream);
+ string flagSortKey = Convert.ToHexString(hash);
+ FlagSortKeyCache[flagPath] = flagSortKey;
+
+ return flagSortKey;
+ }
+ catch
+ {
+ FlagSortKeyCache[flagPath] = flagPath;
+
+ return flagPath;
+ }
+ }
+ }
+}
diff --git a/ServerPickerX/Views/MainWindow.axaml b/ServerPickerX/Views/MainWindow.axaml
index 167d467..ccb08d9 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="980"
- MinWidth="980"
+ Width="920"
+ MinWidth="920"
MaxWidth="1280"
Height="640"
MinHeight="500"
@@ -125,12 +125,12 @@
Height="32"
VerticalAlignment="Center"
BorderThickness="1">
-
-
-
-
-
-
+
+
+
+
+
+
-
-
diff --git a/ServerPickerX/Views/MainWindow.axaml.cs b/ServerPickerX/Views/MainWindow.axaml.cs
index 7a12fff..7d1229f 100644
--- a/ServerPickerX/Views/MainWindow.axaml.cs
+++ b/ServerPickerX/Views/MainWindow.axaml.cs
@@ -151,98 +151,23 @@ private async void ClusterUnclusterBtn_Click(object? sender, Avalonia.Interactiv
RefreshClusterButtonContent();
}
- private async void SavePresetBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
+ private async void PresetsBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
{
if (DataContext is not MainWindowViewModel vm)
{
return;
}
- PresetNameWindow presetNameWindow = new(vm.GetPresetNameSuggestion())
+ PresetManagerWindow presetManagementWindow = new(vm)
{
WindowStartupLocation = WindowStartupLocation.CenterOwner
};
- string? presetName = await presetNameWindow.ShowDialog(this);
+ await presetManagementWindow.ShowDialog(this);
- if (presetName == null)
- {
- return;
- }
-
- presetName = presetName.Trim();
-
- if (string.IsNullOrWhiteSpace(presetName))
- {
- await _messageBoxService.ShowMessageBoxAsync(
- _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
- _localizationService.GetLocaleValue("PresetNameRequiredDialogue")
- );
-
- return;
- }
-
- ServerPresetModel? existingPreset = vm.GetCurrentGamePreset(presetName);
- bool isSuggestedPresetName = vm.IsSuggestedPresetName(presetName);
-
- if (existingPreset != null && !isSuggestedPresetName)
- {
- bool overwriteResult = await _messageBoxService.ShowMessageBoxConfirmationAsync(
- _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
- string.Format(_localizationService.GetLocaleValue("PresetOverwriteConfirmDialogue"), presetName),
- MsBox.Avalonia.Enums.Icon.Setting
- );
-
- if (!overwriteResult)
- {
- return;
- }
- }
-
- _suppressPresetSelectionChanged = true;
-
- try
- {
- await vm.SavePresetAsync(presetName);
-
- SyncPresetSelection(vm.SelectedPreset);
- }
- finally
- {
- _suppressPresetSelectionChanged = false;
- }
- }
-
- private async void DeletePresetBtn_Click(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
- {
- if (DataContext is not MainWindowViewModel vm || vm.SelectedPreset == null)
- {
- return;
- }
-
- bool deleteResult = await _messageBoxService.ShowMessageBoxConfirmationAsync(
- _localizationService.GetLocaleValue("MessageBoxInfoTitle"),
- string.Format(_localizationService.GetLocaleValue("PresetDeleteConfirmDialogue"), vm.SelectedPreset.Name),
- MsBox.Avalonia.Enums.Icon.Warning
- );
-
- if (!deleteResult)
- {
- return;
- }
-
- _suppressPresetSelectionChanged = true;
-
- try
- {
- await vm.DeleteSelectedPresetAsync();
-
- SyncPresetSelection(vm.SelectedPreset);
- }
- finally
- {
- _suppressPresetSelectionChanged = false;
- }
+ vm.LoadPresetPickerItems();
+ SyncPresetSelection(vm.SelectedPreset);
+ RefreshClusterButtonContent();
}
public async Task InitializeApp()
diff --git a/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml b/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml
new file mode 100644
index 0000000..047ba85
--- /dev/null
+++ b/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml
@@ -0,0 +1,262 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml.cs b/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml.cs
new file mode 100644
index 0000000..fba9fe4
--- /dev/null
+++ b/ServerPickerX/Views/UserWindows/PresetManagerWindow.axaml.cs
@@ -0,0 +1,382 @@
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Interactivity;
+using Avalonia.Threading;
+using Avalonia.VisualTree;
+using ServerPickerX.Services.DependencyInjection;
+using ServerPickerX.Services.Localizations;
+using ServerPickerX.Services.MessageBoxes;
+using ServerPickerX.Settings;
+using ServerPickerX.ViewModels;
+using System.Collections;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Threading.Tasks;
+
+namespace ServerPickerX.Views
+{
+ public partial class PresetManagerWindow : Window
+ {
+ private readonly MainWindowViewModel? _mainVm;
+ private bool _allowPresetNameEdit;
+ private bool _committingPresetName;
+ private bool _isEditingPresetName;
+ private string? _editingPresetOriginalName;
+ private ListSortDirection? _presetSortDirection;
+ private string? _serverSortColumn;
+ private ListSortDirection _serverSortDirection = ListSortDirection.Ascending;
+
+ public PresetManagerWindow()
+ {
+ InitializeComponent();
+ }
+
+ public PresetManagerWindow(MainWindowViewModel mainVm)
+ {
+ InitializeComponent();
+ _mainVm = mainVm;
+ DataContext = new PresetManagerWindowViewModel(
+ _mainVm,
+ ServiceLocator.GetRequiredService(),
+ ServiceLocator.GetRequiredService(),
+ ServiceLocator.GetRequiredService()
+ );
+ }
+
+ private async void AddBtn_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ await vm.AddPresetAsync();
+ ReapplyPresetSortIfNeeded();
+ ReapplyServerSortIfNeeded();
+ BeginEditingSelectedPreset();
+ }
+
+ private async void DeleteBtn_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ bool deleted = await vm.DeletePresetsAsync(GetSelectedPresetItems().Select(item => item.Preset).ToList());
+
+ if (deleted)
+ {
+ ReapplyPresetSortIfNeeded();
+ ReapplyServerSortIfNeeded();
+ RestorePresetListFocus();
+ }
+ }
+
+ private async void ApplyBtn_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ if (await vm.ApplySelectedPresetAsync())
+ {
+ Close();
+ }
+ }
+
+ private async void ClusterToggleBtn_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ await vm.ToggleSelectedPresetClusterModeAsync();
+ ReapplyServerSortIfNeeded();
+ }
+
+ private void PresetListGrid_SelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ ReapplyServerSortIfNeeded();
+ }
+
+ private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ e.Handled = true;
+ (TopLevel.GetTopLevel(this) as Window)?.BeginMoveDrag(e);
+ }
+
+ private void PresetListGrid_BeginningEdit(object? sender, DataGridBeginningEditEventArgs e)
+ {
+ if (!_allowPresetNameEdit)
+ {
+ e.Cancel = true;
+ return;
+ }
+
+ if (e.Row.DataContext is not PresetListItemViewModel presetItem)
+ {
+ return;
+ }
+
+ _isEditingPresetName = true;
+ _editingPresetOriginalName = presetItem.Name;
+ }
+
+ private async void PresetListGrid_RowEditEnded(object? sender, DataGridRowEditEndedEventArgs e)
+ {
+ _allowPresetNameEdit = false;
+ _isEditingPresetName = false;
+ PresetListGrid.IsReadOnly = true;
+
+ if (e.EditAction != DataGridEditAction.Commit || e.Row.DataContext is not PresetListItemViewModel presetItem)
+ {
+ return;
+ }
+
+ await CommitPresetNameAsync(presetItem, _editingPresetOriginalName ?? presetItem.Name);
+ }
+
+ private void PresetListGrid_DoubleTapped(object? sender, RoutedEventArgs e)
+ {
+ if (_isEditingPresetName || DataContext is not PresetManagerWindowViewModel vm || vm.SelectedPresetItem == null)
+ {
+ return;
+ }
+
+ if (e.Source is not Control sourceControl ||
+ sourceControl.FindAncestorOfType() != null ||
+ sourceControl.FindAncestorOfType() == null)
+ {
+ return;
+ }
+
+ BeginEditingSelectedPreset();
+ }
+
+ private async Task CommitPresetNameAsync(PresetListItemViewModel presetItem, string originalPresetName)
+ {
+ if (_committingPresetName || DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ _committingPresetName = true;
+
+ try
+ {
+ await vm.RenamePresetAsync(presetItem, originalPresetName);
+ ReapplyPresetSortIfNeeded();
+ ReapplyServerSortIfNeeded();
+ }
+ finally
+ {
+ _committingPresetName = false;
+ _editingPresetOriginalName = null;
+ }
+ }
+
+ private async void PresetListGrid_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ if (e.Key == Key.Delete)
+ {
+ List selectedPresetItems = GetSelectedPresetItems();
+
+ if (selectedPresetItems.Count == 0)
+ {
+ return;
+ }
+
+ e.Handled = true;
+ bool deleted = await vm.DeletePresetsAsync(selectedPresetItems.Select(item => item.Preset).ToList());
+
+ if (deleted)
+ {
+ RestorePresetListFocus();
+ }
+
+ return;
+ }
+
+ if (e.Key == Key.F2)
+ {
+ e.Handled = true;
+ BeginEditingSelectedPreset();
+ }
+ }
+
+ private void PresetListGrid_Sorting(object? sender, DataGridColumnEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm || PresetListGrid.Columns.Count == 0 || !ReferenceEquals(e.Column, PresetListGrid.Columns[0]))
+ {
+ return;
+ }
+
+ _presetSortDirection = _presetSortDirection switch
+ {
+ ListSortDirection.Ascending => ListSortDirection.Descending,
+ _ => ListSortDirection.Ascending,
+ };
+
+ vm.SortPresets(_presetSortDirection.Value);
+ }
+
+ private async void BlockedCheckBox_Click(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ await vm.PersistSelectedPresetServerKeysAsync();
+ ReapplyServerSortIfNeeded();
+ }
+
+ private async void ServerItemsGrid_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (e.Key != Key.Space || DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ if (ServerItemsGrid.SelectedItems.Count == 0)
+ {
+ return;
+ }
+
+ List selectedItems = ServerItemsGrid.SelectedItems
+ .OfType()
+ .ToList();
+
+ if (selectedItems.Count == 0)
+ {
+ return;
+ }
+
+ e.Handled = true;
+
+ bool shouldBlock = selectedItems.Any(serverItem => !serverItem.IsBlocked);
+
+ foreach (PresetServerSelectionItem serverItem in selectedItems)
+ {
+ serverItem.IsBlocked = shouldBlock;
+ }
+
+ await vm.PersistSelectedPresetServerKeysAsync();
+ ReapplyServerSortIfNeeded();
+ }
+
+ private void ServerItemsGrid_Sorting(object? sender, DataGridColumnEventArgs e)
+ {
+ string? sortKey = e.Column.SortMemberPath switch
+ {
+ "IsBlocked" => "Blocked",
+ "FlagSortKey" => "Flag",
+ "Name" => "ServerId",
+ "Description" => "ServerName",
+ _ => null,
+ };
+
+ if (sortKey == null)
+ {
+ return;
+ }
+
+ ListSortDirection defaultDirection = sortKey == "Blocked"
+ ? ListSortDirection.Descending
+ : ListSortDirection.Ascending;
+
+ SortServerItems(sortKey, defaultDirection);
+ }
+
+ private void SortServerItems(string columnName, ListSortDirection defaultDirection = ListSortDirection.Ascending)
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ if (_serverSortColumn == columnName)
+ {
+ _serverSortDirection = _serverSortDirection == ListSortDirection.Ascending
+ ? ListSortDirection.Descending
+ : ListSortDirection.Ascending;
+ }
+ else
+ {
+ _serverSortColumn = columnName;
+ _serverSortDirection = defaultDirection;
+ }
+
+ vm.SortServerItems(columnName, _serverSortDirection);
+ }
+
+ private void ReapplyPresetSortIfNeeded()
+ {
+ if (_presetSortDirection == null || DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ vm.SortPresets(_presetSortDirection.Value);
+ }
+
+ private void ReapplyServerSortIfNeeded()
+ {
+ if (string.IsNullOrWhiteSpace(_serverSortColumn) || DataContext is not PresetManagerWindowViewModel vm)
+ {
+ return;
+ }
+
+ vm.SortServerItems(_serverSortColumn, _serverSortDirection);
+ }
+
+ private void RestorePresetListFocus()
+ {
+ Dispatcher.UIThread.Post(() => PresetListGrid.Focus());
+ }
+
+ private void BeginEditingSelectedPreset()
+ {
+ Dispatcher.UIThread.Post(() =>
+ {
+ if (DataContext is not PresetManagerWindowViewModel vm || vm.SelectedPresetItem == null || GetSelectedPresetItems().Count != 1)
+ {
+ return;
+ }
+
+ vm.StopEditingPresets();
+ PresetListGrid.Focus();
+ _allowPresetNameEdit = true;
+ PresetListGrid.IsReadOnly = false;
+ PresetListGrid.CurrentColumn = PresetListGrid.Columns.FirstOrDefault();
+ PresetListGrid.BeginEdit();
+ });
+ }
+
+ private List GetSelectedPresetItems()
+ {
+ IEnumerable selectedItems = PresetListGrid.SelectedItems ?? System.Array.Empty