launcher: scaffold avalonia main window, view models, and settings dialog
Adds the Avalonia App, MainWindow (900x560 fixed, dark-red banner, left status/progress column, right news panel) and a modal SettingsDialog with language + dev-mode override. MainWindowViewModel binds to the orchestrator through IProgress, throttles UI updates to ~10 Hz via a DispatcherTimer flush, drives state-specific status text and Play-button gating, fetches previous-manifest notes best-effort, and forces a red status banner with Play disabled on a SignatureFailed result. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
8
src/Metin2Launcher/UI/App.axaml
Normal file
8
src/Metin2Launcher/UI/App.axaml
Normal file
@@ -0,0 +1,8 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Metin2Launcher.UI.App"
|
||||
RequestedThemeVariant="Light">
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
28
src/Metin2Launcher/UI/App.axaml.cs
Normal file
28
src/Metin2Launcher/UI/App.axaml.cs
Normal file
@@ -0,0 +1,28 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Metin2Launcher.UI.ViewModels;
|
||||
|
||||
namespace Metin2Launcher.UI;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public static MainWindowViewModel? MainViewModel { get; set; }
|
||||
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
var vm = MainViewModel ?? new MainWindowViewModel();
|
||||
desktop.MainWindow = new MainWindow { DataContext = vm };
|
||||
vm.AttachWindow(desktop.MainWindow);
|
||||
desktop.MainWindow.Opened += async (_, _) => await vm.StartUpdateAsync();
|
||||
}
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
104
src/Metin2Launcher/UI/MainWindow.axaml
Normal file
104
src/Metin2Launcher/UI/MainWindow.axaml
Normal file
@@ -0,0 +1,104 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Metin2Launcher.UI.MainWindow"
|
||||
Width="900" Height="560"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
SystemDecorations="Full"
|
||||
Title="{Binding WindowTitle}">
|
||||
<Grid RowDefinitions="100,*">
|
||||
<!-- Banner -->
|
||||
<Border Grid.Row="0" Background="#8B0000">
|
||||
<Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="2">
|
||||
<TextBlock Text="{Binding BannerTitle}"
|
||||
FontSize="32" FontWeight="Bold"
|
||||
Foreground="White"
|
||||
HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="{Binding BannerVersion}"
|
||||
FontSize="12"
|
||||
Foreground="#FFD7D7"
|
||||
HorizontalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
VerticalAlignment="Top"
|
||||
Margin="0,8,12,0"
|
||||
Spacing="6">
|
||||
<Button Content="⚙" Width="32" Height="32"
|
||||
Foreground="White" Background="Transparent"
|
||||
BorderBrush="#FFFFFF44" BorderThickness="1"
|
||||
ToolTip.Tip="{Binding SettingsTooltip}"
|
||||
Command="{Binding OpenSettingsCommand}"/>
|
||||
<Button Content="✕" Width="32" Height="32"
|
||||
Foreground="White" Background="Transparent"
|
||||
BorderBrush="#FFFFFF44" BorderThickness="1"
|
||||
ToolTip.Tip="{Binding CloseTooltip}"
|
||||
Command="{Binding ExitCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<Grid Grid.Row="1" ColumnDefinitions="300,*">
|
||||
<!-- Left -->
|
||||
<Border Grid.Column="0" Padding="20" Background="#F4F4F4">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0"
|
||||
Text="{Binding StatusText}"
|
||||
FontSize="18" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{Binding StatusForeground}"/>
|
||||
<ProgressBar Grid.Row="1"
|
||||
Margin="0,16,0,4"
|
||||
Height="18"
|
||||
Minimum="0" Maximum="100"
|
||||
Value="{Binding ProgressValue}"
|
||||
IsIndeterminate="{Binding IsIndeterminate}"/>
|
||||
<TextBlock Grid.Row="2"
|
||||
Text="{Binding ProgressPercentText}"
|
||||
FontSize="12"
|
||||
Foreground="#555"/>
|
||||
<TextBlock Grid.Row="3"
|
||||
Margin="0,12,0,0"
|
||||
Text="{Binding DetailText}"
|
||||
FontSize="12"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="#444"/>
|
||||
<StackPanel Grid.Row="4"
|
||||
Orientation="Horizontal"
|
||||
Spacing="10"
|
||||
HorizontalAlignment="Left">
|
||||
<Button Content="{Binding PlayLabel}"
|
||||
Width="110" Height="38"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
Background="#8B0000" Foreground="White"
|
||||
IsEnabled="{Binding CanPlay}"
|
||||
Command="{Binding PlayCommand}"/>
|
||||
<Button Content="{Binding ExitLabel}"
|
||||
Width="110" Height="38"
|
||||
FontSize="14"
|
||||
Command="{Binding ExitCommand}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Right -->
|
||||
<Border Grid.Column="1" Padding="20" Background="White">
|
||||
<DockPanel>
|
||||
<TextBlock DockPanel.Dock="Top"
|
||||
Text="{Binding NewsTitle}"
|
||||
FontSize="16" FontWeight="SemiBold"
|
||||
Margin="0,0,0,8"/>
|
||||
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
|
||||
VerticalScrollBarVisibility="Auto">
|
||||
<TextBlock Text="{Binding NewsText}"
|
||||
TextWrapping="Wrap"
|
||||
FontSize="13"
|
||||
Foreground="#222"/>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
17
src/Metin2Launcher/UI/MainWindow.axaml.cs
Normal file
17
src/Metin2Launcher/UI/MainWindow.axaml.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Metin2Launcher.UI;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
40
src/Metin2Launcher/UI/SettingsDialog.axaml
Normal file
40
src/Metin2Launcher/UI/SettingsDialog.axaml
Normal file
@@ -0,0 +1,40 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="Metin2Launcher.UI.SettingsDialog"
|
||||
Width="460" Height="280"
|
||||
CanResize="False"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
Title="{Binding Title}">
|
||||
<Grid Margin="20" RowDefinitions="Auto,Auto,Auto,Auto,Auto,*,Auto">
|
||||
<TextBlock Grid.Row="0" Text="{Binding LanguageLabel}" FontWeight="SemiBold"/>
|
||||
<ComboBox Grid.Row="1"
|
||||
Margin="0,4,0,12"
|
||||
Width="200" HorizontalAlignment="Left"
|
||||
ItemsSource="{Binding Languages}"
|
||||
SelectedItem="{Binding SelectedLanguage}"
|
||||
DisplayMemberBinding="{Binding Display}"/>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="{Binding ManifestUrlLabel}" FontWeight="SemiBold"/>
|
||||
<TextBox Grid.Row="3"
|
||||
Margin="0,4,0,8"
|
||||
Text="{Binding ManifestUrl}"
|
||||
IsEnabled="{Binding DevMode}"/>
|
||||
<CheckBox Grid.Row="4"
|
||||
Content="{Binding DevModeLabel}"
|
||||
IsChecked="{Binding DevMode}"
|
||||
Margin="0,0,0,4"/>
|
||||
<TextBlock Grid.Row="5"
|
||||
Text="{Binding NoteLabel}"
|
||||
FontSize="11" Foreground="#666"
|
||||
TextWrapping="Wrap"/>
|
||||
|
||||
<StackPanel Grid.Row="6"
|
||||
Orientation="Horizontal"
|
||||
HorizontalAlignment="Right"
|
||||
Spacing="8"
|
||||
Margin="0,12,0,0">
|
||||
<Button Content="{Binding OkLabel}" Width="90" Click="OnOk"/>
|
||||
<Button Content="{Binding CancelLabel}" Width="90" Click="OnCancel"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Window>
|
||||
27
src/Metin2Launcher/UI/SettingsDialog.axaml.cs
Normal file
27
src/Metin2Launcher/UI/SettingsDialog.axaml.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace Metin2Launcher.UI;
|
||||
|
||||
public partial class SettingsDialog : Window
|
||||
{
|
||||
public SettingsDialog()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void InitializeComponent()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
private void OnOk(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Close(true);
|
||||
}
|
||||
|
||||
private void OnCancel(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
Close(false);
|
||||
}
|
||||
}
|
||||
274
src/Metin2Launcher/UI/ViewModels/MainWindowViewModel.cs
Normal file
274
src/Metin2Launcher/UI/ViewModels/MainWindowViewModel.cs
Normal file
@@ -0,0 +1,274 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Metin2Launcher.Config;
|
||||
using Metin2Launcher.GameLaunch;
|
||||
using Metin2Launcher.Localization;
|
||||
using Metin2Launcher.Logging;
|
||||
using Metin2Launcher.Manifest;
|
||||
using Metin2Launcher.Orchestration;
|
||||
|
||||
namespace Metin2Launcher.UI.ViewModels;
|
||||
|
||||
public sealed partial class MainWindowViewModel : ObservableObject
|
||||
{
|
||||
private readonly string _clientRoot;
|
||||
private readonly string _stateDir;
|
||||
private readonly string _settingsPath;
|
||||
private readonly LauncherSettings _settings;
|
||||
private readonly HttpClient _http;
|
||||
private MainWindow? _window;
|
||||
|
||||
// Throttling
|
||||
private UpdateProgress? _pendingProgress;
|
||||
private readonly object _progressLock = new();
|
||||
private DispatcherTimer? _flushTimer;
|
||||
|
||||
[ObservableProperty] private string _statusText = "";
|
||||
[ObservableProperty] private string _detailText = "";
|
||||
[ObservableProperty] private string _newsText = "";
|
||||
[ObservableProperty] private double _progressValue;
|
||||
[ObservableProperty] private bool _isIndeterminate;
|
||||
[ObservableProperty] private bool _canPlay;
|
||||
[ObservableProperty] private IBrush _statusForeground = Brushes.Black;
|
||||
[ObservableProperty] private string _progressPercentText = "";
|
||||
|
||||
public string WindowTitle => Loc.Get("window.title");
|
||||
public string BannerTitle => Loc.Get("banner.title");
|
||||
public string BannerVersion => Loc.Get("banner.version", "0.1-dev");
|
||||
public string PlayLabel => Loc.Get("button.play");
|
||||
public string ExitLabel => Loc.Get("button.exit");
|
||||
public string SettingsTooltip => Loc.Get("button.settings");
|
||||
public string CloseTooltip => Loc.Get("button.close");
|
||||
public string NewsTitle => Loc.Get("news.title");
|
||||
|
||||
private LauncherState _state = LauncherState.Idle;
|
||||
private LoadedManifest? _lastManifest;
|
||||
private bool _signatureBlocked;
|
||||
|
||||
public MainWindowViewModel()
|
||||
{
|
||||
_clientRoot = Directory.GetCurrentDirectory();
|
||||
_stateDir = Path.Combine(_clientRoot, LauncherConfig.StateDirName);
|
||||
Directory.CreateDirectory(_stateDir);
|
||||
_settingsPath = Path.Combine(_stateDir, "launcher-settings.json");
|
||||
_settings = LauncherSettings.Load(_settingsPath);
|
||||
Loc.SetLocale(_settings.Locale);
|
||||
Loc.Changed += OnLocaleChanged;
|
||||
|
||||
_http = new HttpClient { Timeout = LauncherConfig.BlobFetchTimeout };
|
||||
_http.DefaultRequestHeaders.UserAgent.ParseAdd("Metin2Launcher/0.1");
|
||||
|
||||
_statusText = Loc.Get("status.idle");
|
||||
_newsText = "";
|
||||
}
|
||||
|
||||
public void AttachWindow(Window w) => _window = w as MainWindow;
|
||||
|
||||
private void OnLocaleChanged()
|
||||
{
|
||||
OnPropertyChanged(nameof(WindowTitle));
|
||||
OnPropertyChanged(nameof(BannerTitle));
|
||||
OnPropertyChanged(nameof(BannerVersion));
|
||||
OnPropertyChanged(nameof(PlayLabel));
|
||||
OnPropertyChanged(nameof(ExitLabel));
|
||||
OnPropertyChanged(nameof(SettingsTooltip));
|
||||
OnPropertyChanged(nameof(CloseTooltip));
|
||||
OnPropertyChanged(nameof(NewsTitle));
|
||||
// Refresh status text from current state
|
||||
ApplyState(_state);
|
||||
}
|
||||
|
||||
public async Task StartUpdateAsync()
|
||||
{
|
||||
// ~10 Hz UI flush timer
|
||||
_flushTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(100) };
|
||||
_flushTimer.Tick += (_, _) => FlushProgress();
|
||||
_flushTimer.Start();
|
||||
|
||||
var orchestrator = new UpdateOrchestrator(_clientRoot, _settings, _http);
|
||||
var progress = new Progress<UpdateProgress>(p =>
|
||||
{
|
||||
lock (_progressLock) { _pendingProgress = p; }
|
||||
});
|
||||
|
||||
UpdateOrchestrator.Result? result = null;
|
||||
try
|
||||
{
|
||||
using var cts = new CancellationTokenSource(LauncherConfig.TotalUpdateTimeout);
|
||||
result = await Task.Run(() => orchestrator.RunAsync(progress, cts.Token));
|
||||
}
|
||||
catch (ManifestSignatureException)
|
||||
{
|
||||
_signatureBlocked = true;
|
||||
ApplyState(LauncherState.SignatureFailed);
|
||||
FlushProgress();
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error("update flow crashed, falling back to offline launch", ex);
|
||||
ApplyState(LauncherState.OfflineFallback);
|
||||
}
|
||||
|
||||
FlushProgress();
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
_lastManifest = result.Manifest;
|
||||
ApplyState(result.FinalState);
|
||||
await TryFetchNewsAsync(result.Manifest);
|
||||
}
|
||||
|
||||
_flushTimer?.Stop();
|
||||
}
|
||||
|
||||
private void FlushProgress()
|
||||
{
|
||||
UpdateProgress? p;
|
||||
lock (_progressLock) { p = _pendingProgress; _pendingProgress = null; }
|
||||
if (p is null) return;
|
||||
|
||||
if (p.Manifest is not null) _lastManifest = p.Manifest;
|
||||
ApplyState(p.State);
|
||||
|
||||
if (p.Percent.HasValue)
|
||||
{
|
||||
IsIndeterminate = false;
|
||||
ProgressValue = p.Percent.Value;
|
||||
ProgressPercentText = $"{(int)p.Percent.Value}%";
|
||||
}
|
||||
else
|
||||
{
|
||||
IsIndeterminate = true;
|
||||
ProgressPercentText = "";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(p.Detail))
|
||||
DetailText = p.Detail!;
|
||||
}
|
||||
|
||||
private void ApplyState(LauncherState s)
|
||||
{
|
||||
_state = s;
|
||||
StatusText = s switch
|
||||
{
|
||||
LauncherState.Idle => Loc.Get("status.idle"),
|
||||
LauncherState.FetchingManifest => Loc.Get("status.fetchingManifest"),
|
||||
LauncherState.VerifyingSignature => Loc.Get("status.verifyingSignature"),
|
||||
LauncherState.Diffing => Loc.Get("status.diffing"),
|
||||
LauncherState.Downloading => Loc.Get("status.downloading"),
|
||||
LauncherState.Applying => Loc.Get("status.applying"),
|
||||
LauncherState.UpToDate => Loc.Get("status.upToDate"),
|
||||
LauncherState.OfflineFallback => Loc.Get("status.offlineFallback"),
|
||||
LauncherState.SignatureFailed => Loc.Get("status.signatureFailed"),
|
||||
LauncherState.Launching => Loc.Get("status.launching"),
|
||||
_ => "",
|
||||
};
|
||||
|
||||
StatusForeground = s == LauncherState.SignatureFailed ? Brushes.DarkRed : Brushes.Black;
|
||||
|
||||
var indeterminate = s is LauncherState.FetchingManifest
|
||||
or LauncherState.VerifyingSignature
|
||||
or LauncherState.Diffing;
|
||||
IsIndeterminate = indeterminate;
|
||||
|
||||
if (s is LauncherState.UpToDate or LauncherState.OfflineFallback)
|
||||
{
|
||||
ProgressValue = 100;
|
||||
ProgressPercentText = "100%";
|
||||
}
|
||||
|
||||
CanPlay = !_signatureBlocked
|
||||
&& s is LauncherState.UpToDate
|
||||
or LauncherState.OfflineFallback;
|
||||
}
|
||||
|
||||
private async Task TryFetchNewsAsync(LoadedManifest? loaded)
|
||||
{
|
||||
if (loaded is null)
|
||||
{
|
||||
NewsText = Loc.Get("news.empty");
|
||||
return;
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"v{loaded.Manifest.Version} ({loaded.Manifest.CreatedAt})");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(string.IsNullOrWhiteSpace(loaded.Manifest.Notes) ? "—" : loaded.Manifest.Notes);
|
||||
|
||||
var prev = loaded.Manifest.Previous;
|
||||
if (!string.IsNullOrEmpty(prev))
|
||||
{
|
||||
try
|
||||
{
|
||||
var manifestUrl = _settings.EffectiveManifestUrl();
|
||||
var baseUri = new Uri(manifestUrl);
|
||||
var prevUrl = new Uri(baseUri, $"manifests/{prev}.json").ToString();
|
||||
var json = await _http.GetStringAsync(prevUrl);
|
||||
var prevManifest = System.Text.Json.JsonSerializer.Deserialize<Manifest.Manifest>(json);
|
||||
if (prevManifest is not null && !string.IsNullOrWhiteSpace(prevManifest.Notes))
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine();
|
||||
sb.AppendLine(Loc.Get("news.previousHeader", prev));
|
||||
sb.AppendLine(prevManifest.Notes);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// best effort
|
||||
}
|
||||
}
|
||||
|
||||
NewsText = sb.ToString();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Exit()
|
||||
{
|
||||
if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime d)
|
||||
d.Shutdown(0);
|
||||
else
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Play()
|
||||
{
|
||||
if (!CanPlay) return;
|
||||
ApplyState(LauncherState.Launching);
|
||||
var ok = GameProcess.Launch(_clientRoot, LauncherConfig.GameExecutable);
|
||||
if (ok)
|
||||
{
|
||||
if (Avalonia.Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime d)
|
||||
d.Shutdown(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Re-enable so user can retry
|
||||
CanPlay = true;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task OpenSettings()
|
||||
{
|
||||
if (_window is null) return;
|
||||
var dialogVm = new SettingsDialogViewModel(_settings);
|
||||
var dialog = new SettingsDialog { DataContext = dialogVm };
|
||||
var ok = await dialog.ShowDialog<bool>(_window);
|
||||
if (!ok) return;
|
||||
|
||||
dialogVm.ApplyTo(_settings);
|
||||
_settings.Save(_settingsPath);
|
||||
Loc.SetLocale(_settings.Locale);
|
||||
}
|
||||
}
|
||||
50
src/Metin2Launcher/UI/ViewModels/SettingsDialogViewModel.cs
Normal file
50
src/Metin2Launcher/UI/ViewModels/SettingsDialogViewModel.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using Metin2Launcher.Config;
|
||||
using Metin2Launcher.Localization;
|
||||
|
||||
namespace Metin2Launcher.UI.ViewModels;
|
||||
|
||||
public sealed class LanguageOption
|
||||
{
|
||||
public string Code { get; }
|
||||
public string Display { get; }
|
||||
public LanguageOption(string code, string display) { Code = code; Display = display; }
|
||||
public override string ToString() => Display;
|
||||
}
|
||||
|
||||
public sealed partial class SettingsDialogViewModel : ObservableObject
|
||||
{
|
||||
[ObservableProperty] private LanguageOption _selectedLanguage;
|
||||
[ObservableProperty] private string _manifestUrl;
|
||||
[ObservableProperty] private bool _devMode;
|
||||
|
||||
public List<LanguageOption> Languages { get; }
|
||||
|
||||
public string Title => Loc.Get("settings.title");
|
||||
public string LanguageLabel => Loc.Get("settings.language");
|
||||
public string ManifestUrlLabel => Loc.Get("settings.manifestUrl");
|
||||
public string DevModeLabel => Loc.Get("settings.devMode");
|
||||
public string NoteLabel => Loc.Get("settings.note");
|
||||
public string OkLabel => Loc.Get("button.ok");
|
||||
public string CancelLabel => Loc.Get("button.cancel");
|
||||
|
||||
public SettingsDialogViewModel(LauncherSettings settings)
|
||||
{
|
||||
Languages = new List<LanguageOption>
|
||||
{
|
||||
new("cs", Loc.Get("settings.language.cs")),
|
||||
new("en", Loc.Get("settings.language.en")),
|
||||
};
|
||||
_selectedLanguage = Languages.Find(l => l.Code == settings.Locale) ?? Languages[0];
|
||||
_manifestUrl = settings.ManifestUrlOverride ?? LauncherConfig.ManifestUrl;
|
||||
_devMode = settings.DevMode;
|
||||
}
|
||||
|
||||
public void ApplyTo(LauncherSettings settings)
|
||||
{
|
||||
settings.Locale = SelectedLanguage.Code;
|
||||
settings.DevMode = DevMode;
|
||||
settings.ManifestUrlOverride = ManifestUrl == LauncherConfig.ManifestUrl ? null : ManifestUrl;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user