Files
LanMountainDesktop/LanMountainDesktop/ViewModels/SettingsViewModels.cs
2026-03-20 18:05:42 +08:00

2267 lines
80 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.Settings.Core;
namespace LanMountainDesktop.ViewModels;
public sealed partial class SettingsWindowViewModel : ViewModelBase
{
private readonly LocalizationService _localizationService;
private string _languageCode;
private string _defaultRestartMessage = string.Empty;
public SettingsWindowViewModel()
{
_localizationService = new();
_languageCode = "zh-CN";
}
public SettingsWindowViewModel(LocalizationService localizationService, string languageCode)
{
_localizationService = localizationService;
_languageCode = languageCode;
}
private string L(string key) => _localizationService.GetString(_languageCode, key, key);
[ObservableProperty]
private string _title = string.Empty;
[ObservableProperty]
private string _currentPageTitle = string.Empty;
[ObservableProperty]
private bool _isPageTitleVisible = true;
[ObservableProperty]
private string? _currentPageDescription;
[ObservableProperty]
private string? _currentPageId;
[ObservableProperty]
private bool _isRestartRequested;
[ObservableProperty]
private string _restartMessage = string.Empty;
[ObservableProperty]
private string _restartTitle = string.Empty;
[ObservableProperty]
private string _restartButtonText = string.Empty;
[ObservableProperty]
private string _restartDialogTitle = string.Empty;
[ObservableProperty]
private string _restartDialogPrimaryText = string.Empty;
[ObservableProperty]
private string _restartDialogCloseText = string.Empty;
[ObservableProperty]
private string? _drawerTitle;
[ObservableProperty]
private string _drawerFallbackTitle = string.Empty;
[ObservableProperty]
private bool _isDrawerOpen;
public SettingsWindowViewModel Initialize()
{
RefreshLanguage(_languageCode);
CurrentPageTitle = Title;
return this;
}
public void RefreshLanguage(string? languageCode)
{
_languageCode = _localizationService.NormalizeLanguageCode(languageCode);
Title = L("settings.title");
RestartTitle = L("settings.restart_dock.title");
RestartButtonText = L("settings.restart_dock.button");
RestartDialogTitle = L("settings.restart_dialog.title");
RestartDialogPrimaryText = L("settings.restart_dialog.restart");
RestartDialogCloseText = _localizationService.GetString(
_languageCode,
"settings.restart_dialog.later",
L("settings.restart_dialog.cancel"));
DrawerFallbackTitle = L("settings.window.drawer_default");
var nextDefaultRestartMessage = L("settings.restart_dock.description");
if (string.IsNullOrWhiteSpace(RestartMessage) || string.Equals(RestartMessage, _defaultRestartMessage, StringComparison.Ordinal))
{
RestartMessage = nextDefaultRestartMessage;
}
_defaultRestartMessage = nextDefaultRestartMessage;
}
public string GetDefaultRestartMessage() => _defaultRestartMessage;
public ObservableCollection<SettingsPageDescriptor> Pages { get; } = [];
}
public sealed class SelectionOption
{
public SelectionOption(string value, string label)
{
Value = value;
Label = label;
}
public string Value { get; }
public string Label { get; }
}
public sealed class ThemeSeedCandidateOption
{
public ThemeSeedCandidateOption(string value, string label, Color color, bool isSelected)
{
Value = value;
Label = label;
Color = color;
IsSelected = isSelected;
Brush = new SolidColorBrush(color);
}
public string Value { get; }
public string Label { get; }
public Color Color { get; }
public bool IsSelected { get; }
public IBrush Brush { get; }
}
public sealed class TimeZoneOption
{
public TimeZoneOption(string? id, string label)
{
Id = id;
Label = label;
}
public string? Id { get; }
public string Label { get; }
}
public sealed partial class GeneralSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly TimeZoneService _timeZoneService;
private readonly LocalizationService _localizationService = new();
private readonly string _startupRenderMode;
private string _languageCode;
private bool _isInitializing;
public GeneralSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_timeZoneService = settingsFacade.Region.GetTimeZoneService();
_startupRenderMode = Program.StartupRenderMode;
var regionState = _settingsFacade.Region.Get();
_languageCode = _localizationService.NormalizeLanguageCode(regionState.LanguageCode);
Languages = CreateLanguageOptions();
RenderModes = CreateRenderModeOptions();
TimeZones = CreateTimeZoneOptions();
RefreshLocalizedText();
_isInitializing = true;
SelectedLanguage = Languages.FirstOrDefault(option =>
string.Equals(option.Value, regionState.LanguageCode, StringComparison.OrdinalIgnoreCase))
?? Languages[0];
SelectedTimeZone = TimeZones.FirstOrDefault(option =>
string.Equals(option.Id, regionState.TimeZoneId, StringComparison.OrdinalIgnoreCase))
?? TimeZones[0];
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var normalizedRenderMode = AppRenderingModeHelper.Normalize(appSnapshot.AppRenderMode);
SelectedRenderMode = RenderModes.FirstOrDefault(option =>
string.Equals(option.Value, normalizedRenderMode, StringComparison.OrdinalIgnoreCase))
?? RenderModes[0];
_isInitializing = false;
RefreshPreview();
}
public event Action? RestartRequested;
public IReadOnlyList<SelectionOption> Languages { get; }
public IReadOnlyList<SelectionOption> RenderModes { get; }
public IReadOnlyList<TimeZoneOption> TimeZones { get; }
[ObservableProperty]
private SelectionOption _selectedLanguage = new("zh-CN", "中文");
[ObservableProperty]
private TimeZoneOption _selectedTimeZone = new(null, "Follow system default");
[ObservableProperty]
private SelectionOption _selectedRenderMode = new(AppRenderingModeHelper.Default, "Default");
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _basicHeader = string.Empty;
[ObservableProperty]
private string _runtimeHeader = string.Empty;
[ObservableProperty]
private string _runtimeDescription = string.Empty;
[ObservableProperty]
private string _languageHeader = string.Empty;
[ObservableProperty]
private string _timeZoneHeader = string.Empty;
[ObservableProperty]
private string _timeZoneDescription = string.Empty;
[ObservableProperty]
private string _renderModeHeader = string.Empty;
[ObservableProperty]
private string _previewHeader = string.Empty;
[ObservableProperty]
private string _previewTimeLabel = string.Empty;
[ObservableProperty]
private string _previewDateLabel = string.Empty;
[ObservableProperty]
private string _previewTimeText = string.Empty;
[ObservableProperty]
private string _previewDateText = string.Empty;
[ObservableProperty]
private string _renderModeRestartMessage = string.Empty;
partial void OnSelectedLanguageChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
// 更新语言代码并刷新UI文本
_languageCode = _localizationService.NormalizeLanguageCode(value.Value);
RefreshLocalizedText();
RefreshPreview();
// 保存设置
_settingsFacade.Region.Save(new RegionSettingsState(
value.Value,
NormalizeTimeZoneId(SelectedTimeZone?.Id)));
}
partial void OnSelectedTimeZoneChanged(TimeZoneOption value)
{
RefreshPreview();
if (_isInitializing || value is null)
{
return;
}
_settingsFacade.Region.Save(new RegionSettingsState(
SelectedLanguage.Value,
NormalizeTimeZoneId(value.Id)));
}
partial void OnSelectedRenderModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
var appSnapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
var normalizedRenderMode = AppRenderingModeHelper.Normalize(value.Value);
appSnapshot.AppRenderMode = normalizedRenderMode;
_settingsFacade.Settings.SaveSnapshot(
SettingsScope.App,
appSnapshot,
changedKeys: [nameof(AppSettingsSnapshot.AppRenderMode)]);
var restartRequired = !string.Equals(_startupRenderMode, normalizedRenderMode, StringComparison.OrdinalIgnoreCase);
PendingRestartStateService.SetPending(PendingRestartStateService.RenderModeReason, restartRequired);
if (restartRequired)
{
RestartRequested?.Invoke();
}
}
private IReadOnlyList<SelectionOption> CreateLanguageOptions()
{
return
[
new SelectionOption("zh-CN", L("settings.region.language_zh", "中文")),
new SelectionOption("en-US", L("settings.region.language_en", "English"))
];
}
private IReadOnlyList<SelectionOption> CreateRenderModeOptions()
{
return
[
new SelectionOption(AppRenderingModeHelper.Default, L("settings.about.render_mode.default", "Default")),
new SelectionOption(AppRenderingModeHelper.Software, L("settings.about.render_mode.software", "Software")),
new SelectionOption(AppRenderingModeHelper.AngleEgl, L("settings.about.render_mode.angle_egl", "Angle EGL")),
new SelectionOption(AppRenderingModeHelper.Wgl, L("settings.about.render_mode.wgl", "WGL")),
new SelectionOption(AppRenderingModeHelper.Vulkan, L("settings.about.render_mode.vulkan", "Vulkan"))
];
}
private IReadOnlyList<TimeZoneOption> CreateTimeZoneOptions()
{
return _timeZoneService
.GetAllTimeZones()
.Select(zone => new TimeZoneOption(zone.Id, _timeZoneService.GetTimeZoneDisplayName(zone)))
.Prepend(new TimeZoneOption(null, L("settings.region.follow_system", "Follow system default")))
.ToList();
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.general.title", "General");
PageDescription = L("settings.general.description", "Core language, time zone, and runtime behavior.");
BasicHeader = L("settings.general.basic_header", "Basic Settings");
RuntimeHeader = L("settings.general.runtime_header", "Runtime");
RuntimeDescription = L(
"settings.about.render_mode_desc",
"Choose the rendering backend. Restart the app after changing this option.");
LanguageHeader = L("settings.region.language_header", "Language");
TimeZoneHeader = L("settings.region.timezone_header", "Time Zone");
TimeZoneDescription = L(
"settings.region.timezone_desc",
"Select a time zone. Clock and calendar widgets will follow this zone.");
RenderModeHeader = L("settings.about.render_mode_header", "App Rendering Mode");
PreviewHeader = L("settings.general.preview_header", "Date & Time Preview");
PreviewTimeLabel = L("settings.general.preview_time_label", "Time");
PreviewDateLabel = L("settings.general.preview_date_label", "Date");
RenderModeRestartMessage = L(
"settings.general.render_mode_restart_message",
"Rendering mode changes require restarting the app.");
}
private void RefreshPreview()
{
var culture = ResolveCulture(SelectedLanguage?.Value ?? _languageCode);
var timeZone = ResolveSelectedTimeZone();
var now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZone);
PreviewTimeText = now.ToString("T", culture);
PreviewDateText = now.ToString("D", culture);
}
private TimeZoneInfo ResolveSelectedTimeZone()
{
var timeZoneId = NormalizeTimeZoneId(SelectedTimeZone?.Id);
if (string.IsNullOrWhiteSpace(timeZoneId))
{
return TimeZoneInfo.Local;
}
try
{
return TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
}
catch (TimeZoneNotFoundException)
{
return TimeZoneInfo.Local;
}
catch (InvalidTimeZoneException)
{
return TimeZoneInfo.Local;
}
}
private string? NormalizeTimeZoneId(string? value)
=> string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private CultureInfo ResolveCulture(string? languageCode)
{
var normalizedLanguageCode = _localizationService.NormalizeLanguageCode(languageCode);
try
{
return CultureInfo.GetCultureInfo(normalizedLanguageCode);
}
catch (CultureNotFoundException)
{
return CultureInfo.GetCultureInfo("zh-CN");
}
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
{
private static readonly Color DefaultSeedColor = Color.Parse("#FF3B82F6");
private static readonly SolidColorBrush NeutralLightBrushValue = new(Color.Parse("#FFFFFFFF"));
private static readonly SolidColorBrush NeutralDarkBrushValue = new(Color.Parse("#FF000000"));
private readonly ISettingsFacadeService _settingsFacade;
private readonly IAppearanceThemeService _appearanceThemeService;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
private string? _selectedWallpaperSeed;
public AppearanceSettingsPageViewModel(
ISettingsFacadeService settingsFacade,
IAppearanceThemeService appearanceThemeService)
{
_settingsFacade = settingsFacade;
_appearanceThemeService = appearanceThemeService;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
ThemeColorModes = CreateThemeColorModes();
_isInitializing = true;
Load();
_isInitializing = false;
}
public event Action<string>? RestartRequested;
public IReadOnlyList<SelectionOption> ThemeColorModes { get; }
[ObservableProperty]
private IReadOnlyList<SelectionOption> _systemMaterialModes = [];
[ObservableProperty]
private bool _isNightMode;
[ObservableProperty]
private string _themeColor = string.Empty;
[ObservableProperty]
private Color _customSeedPickerValue = DefaultSeedColor;
partial void OnCustomSeedPickerValueChanged(Color value)
{
if (_isInitializing ||
!string.Equals(SelectedThemeColorMode?.Value, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase))
{
return;
}
UpdatePreview(BuildPendingState(usePickerSeed: true));
}
[ObservableProperty]
private bool _useSystemChrome;
[ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
[ObservableProperty]
private SelectionOption _selectedThemeColorMode = new(ThemeAppearanceValues.ColorModeSeedMonet, "User theme color Monet");
[ObservableProperty]
private SelectionOption _selectedSystemMaterialMode = new(ThemeAppearanceValues.MaterialNone, "None");
[ObservableProperty]
private bool _isThemeColorEditable;
[ObservableProperty]
private bool _isWallpaperMode;
[ObservableProperty]
private bool _showNeutralPreview;
[ObservableProperty]
private bool _showMonetPreview;
[ObservableProperty]
private bool _isWallpaperSeedSelectable;
[ObservableProperty]
private string _themeColorSourceDescription = string.Empty;
[ObservableProperty]
private string _systemMaterialDescription = string.Empty;
[ObservableProperty]
private IBrush _primarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
[ObservableProperty]
private IBrush _secondarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
[ObservableProperty]
private IBrush _tertiarySwatchBrush = new SolidColorBrush(DefaultSeedColor);
[ObservableProperty]
private IBrush _neutralSwatchBrush = new SolidColorBrush(Color.Parse("#FFF2F4F7"));
[ObservableProperty]
private IBrush _seedSwatchBrush = new SolidColorBrush(DefaultSeedColor);
[ObservableProperty]
private IReadOnlyList<ThemeSeedCandidateOption> _wallpaperSeedCandidates = [];
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _nightModeLabel = string.Empty;
[ObservableProperty]
private string _useSystemChromeLabel = string.Empty;
[ObservableProperty]
private string _themeColorLabel = string.Empty;
[ObservableProperty]
private string _themeColorModeLabel = string.Empty;
[ObservableProperty]
private string _systemMaterialLabel = string.Empty;
[ObservableProperty]
private string _globalCornerRadiusLabel = string.Empty;
[ObservableProperty]
private string _globalCornerRadiusDescription = string.Empty;
[ObservableProperty]
private string _themeHeader = string.Empty;
[ObservableProperty]
private string _themeSourceNeutralText = string.Empty;
[ObservableProperty]
private string _themeSourceUserColorText = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperText = string.Empty;
[ObservableProperty]
private string _themeSourceDefaultDescription = string.Empty;
[ObservableProperty]
private string _themeSourceUserColorDescription = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperDescription = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperAppDescription = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperSystemDescription = string.Empty;
[ObservableProperty]
private string _themeSourceWallpaperFallbackDescription = string.Empty;
[ObservableProperty]
private string _systemMaterialNoneText = string.Empty;
[ObservableProperty]
private string _systemMaterialMicaText = string.Empty;
[ObservableProperty]
private string _systemMaterialAcrylicText = string.Empty;
[ObservableProperty]
private string _systemMaterialSwitchableDescription = string.Empty;
[ObservableProperty]
private string _systemMaterialFixedDescription = string.Empty;
[ObservableProperty]
private string _appearanceRestartMessage = string.Empty;
[ObservableProperty]
private string _previewPrimaryLabel = string.Empty;
[ObservableProperty]
private string _previewSecondaryLabel = string.Empty;
[ObservableProperty]
private string _previewTertiaryLabel = string.Empty;
[ObservableProperty]
private string _previewNeutralLabel = string.Empty;
[ObservableProperty]
private string _previewSeedLabel = string.Empty;
[ObservableProperty]
private string _previewNeutralLightLabel = string.Empty;
[ObservableProperty]
private string _previewNeutralDarkLabel = string.Empty;
[ObservableProperty]
private string _seedApplyButtonText = string.Empty;
[ObservableProperty]
private string _wallpaperSeedFlyoutTitle = string.Empty;
[ObservableProperty]
private string _wallpaperSeedCurrentText = string.Empty;
public IBrush NeutralLightPreviewBrush => NeutralLightBrushValue;
public IBrush NeutralDarkPreviewBrush => NeutralDarkBrushValue;
public void Load()
{
var theme = _settingsFacade.Theme.Get();
var liveSnapshot = _appearanceThemeService.GetCurrent();
RefreshMaterialModeOptions(liveSnapshot);
_isInitializing = true;
try
{
ApplySavedState(theme);
}
finally
{
_isInitializing = false;
}
UpdatePreview(theme);
}
partial void OnIsNightModeChanged(bool value)
{
if (_isInitializing)
{
return;
}
PersistCurrentState(restartRequired: false);
}
partial void OnUseSystemChromeChanged(bool value)
{
if (_isInitializing)
{
return;
}
PersistCurrentState(restartRequired: false);
}
partial void OnGlobalCornerRadiusScaleChanged(double value)
{
if (_isInitializing)
{
return;
}
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
if (Math.Abs(normalized - value) > 0.0001d)
{
_isInitializing = true;
try
{
GlobalCornerRadiusScale = normalized;
}
finally
{
_isInitializing = false;
}
return;
}
PersistCurrentState(restartRequired: false);
}
partial void OnSelectedThemeColorModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
PersistCurrentState(restartRequired: true);
}
partial void OnSelectedSystemMaterialModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
PersistCurrentState(restartRequired: true);
}
[RelayCommand]
private void ApplyCustomSeed()
{
if (!IsThemeColorEditable)
{
return;
}
ThemeColor = CustomSeedPickerValue.ToString();
PersistCurrentState(restartRequired: false);
}
public void CancelCustomSeedPreview()
{
if (_isInitializing)
{
return;
}
SyncCustomSeedPickerWithSavedThemeColor();
UpdatePreview(BuildPendingState(usePickerSeed: false));
}
public void SelectWallpaperSeed(string value)
{
if (!IsWallpaperMode || string.IsNullOrWhiteSpace(value))
{
return;
}
_selectedWallpaperSeed = value;
PersistCurrentState(restartRequired: true);
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.appearance.title", "Appearance");
PageDescription = L("settings.appearance.description", "Adjust theme source, material background, and window chrome.");
ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
SystemMaterialLabel = L("settings.appearance.system_material_label", "System material");
GlobalCornerRadiusLabel = L("settings.appearance.corner_radius.label", "Global corner radius");
GlobalCornerRadiusDescription = L("settings.appearance.corner_radius.description", "Adjust the shared radius scale used by cards, panels, and component containers.");
ThemeSourceNeutralText = L("settings.appearance.theme_color_mode.neutral", "Default neutral");
ThemeSourceUserColorText = L("settings.appearance.theme_color_mode.user", "User theme color Monet");
ThemeSourceWallpaperText = L("settings.appearance.theme_color_mode.wallpaper", "Wallpaper Monet");
ThemeSourceDefaultDescription = L("settings.appearance.theme_color_mode_desc.neutral", "Use the standard light and dark neutral surfaces.");
ThemeSourceUserColorDescription = L("settings.appearance.theme_color_mode_desc.user", "Use the selected theme color as the Monet seed.");
ThemeSourceWallpaperDescription = L("settings.appearance.theme_color_mode_desc.wallpaper", "Use the current wallpaper palette. App wallpaper is preferred, then system wallpaper.");
ThemeSourceWallpaperAppDescription = L("settings.appearance.theme_color_preview.app", "Currently previewing colors extracted from the app wallpaper.");
ThemeSourceWallpaperSystemDescription = L("settings.appearance.theme_color_preview.system", "Currently previewing colors extracted from the system wallpaper.");
ThemeSourceWallpaperFallbackDescription = L("settings.appearance.theme_color_preview.fallback", "No usable wallpaper was found. The app is using a fallback accent.");
SystemMaterialNoneText = L("settings.appearance.system_material.none", "None");
SystemMaterialMicaText = L("settings.appearance.system_material.mica", "Mica");
SystemMaterialAcrylicText = L("settings.appearance.system_material.acrylic", "Acrylic");
SystemMaterialSwitchableDescription = L("settings.appearance.system_material_desc.switchable", "Apply the selected material to windows, Dock, status bar, and component hosts.");
SystemMaterialFixedDescription = L("settings.appearance.system_material_desc.fixed", "Your current system only exposes the available material modes listed here.");
AppearanceRestartMessage = L(
"settings.appearance.restart_message",
"Theme source and system material changes require restarting the app.");
PreviewPrimaryLabel = L("settings.appearance.preview.primary", "Primary");
PreviewSecondaryLabel = L("settings.appearance.preview.secondary", "Secondary");
PreviewTertiaryLabel = L("settings.appearance.preview.tertiary", "Tertiary");
PreviewNeutralLabel = L("settings.appearance.preview.neutral", "Neutral");
PreviewSeedLabel = L("settings.appearance.preview.seed", "Seed");
PreviewNeutralLightLabel = L("settings.appearance.preview.neutral_light", "White");
PreviewNeutralDarkLabel = L("settings.appearance.preview.neutral_dark", "Black");
SeedApplyButtonText = L("settings.appearance.preview.apply_seed", "Apply");
WallpaperSeedFlyoutTitle = L("settings.appearance.preview.wallpaper_candidates", "Wallpaper seed candidates");
WallpaperSeedCurrentText = L("settings.appearance.preview.wallpaper_current", "Current");
}
private void RefreshMaterialModeOptions(AppearanceThemeSnapshot snapshot)
{
SystemMaterialModes = snapshot.AvailableSystemMaterialModes
.Select(value => new SelectionOption(value, ResolveMaterialModeLabel(value)))
.ToList();
SystemMaterialDescription = snapshot.CanChangeSystemMaterial
? SystemMaterialSwitchableDescription
: SystemMaterialFixedDescription;
}
private void ApplySavedState(ThemeAppearanceSettingsState theme)
{
IsNightMode = theme.IsNightMode;
ThemeColor = theme.ThemeColor ?? string.Empty;
UseSystemChrome = theme.UseSystemChrome;
GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
_selectedWallpaperSeed = theme.SelectedWallpaperSeed;
SyncCustomSeedPickerWithSavedThemeColor();
var savedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(theme.ThemeColorMode, theme.ThemeColor);
var savedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(theme.SystemMaterialMode);
SelectedThemeColorMode = ThemeColorModes.FirstOrDefault(option =>
string.Equals(option.Value, savedThemeColorMode, StringComparison.OrdinalIgnoreCase))
?? ThemeColorModes[0];
SelectedSystemMaterialMode = SystemMaterialModes.FirstOrDefault(option =>
string.Equals(option.Value, savedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
?? SystemMaterialModes[0];
}
private void PersistCurrentState(bool restartRequired)
{
var pendingState = BuildPendingState(usePickerSeed: false);
_settingsFacade.Theme.Save(pendingState);
var savedState = _settingsFacade.Theme.Get();
_isInitializing = true;
try
{
ApplySavedState(savedState);
}
finally
{
_isInitializing = false;
}
RefreshMaterialModeOptions(_appearanceThemeService.GetCurrent());
UpdatePreview(savedState);
if (restartRequired)
{
RestartRequested?.Invoke(AppearanceRestartMessage);
}
}
private ThemeAppearanceSettingsState BuildPendingState(bool usePickerSeed)
{
var themeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(SelectedThemeColorMode?.Value, ThemeColor);
var themeColor = themeColorMode == ThemeAppearanceValues.ColorModeSeedMonet
? (usePickerSeed ? CustomSeedPickerValue.ToString() : string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor)
: string.IsNullOrWhiteSpace(ThemeColor) ? null : ThemeColor;
return new ThemeAppearanceSettingsState(
IsNightMode,
themeColor,
UseSystemChrome,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
themeColorMode,
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
_selectedWallpaperSeed);
}
private void UpdatePreview(ThemeAppearanceSettingsState pendingState)
{
var preview = _appearanceThemeService.BuildPreview(pendingState);
var normalizedMode = preview.ThemeColorMode;
ShowNeutralPreview = normalizedMode == ThemeAppearanceValues.ColorModeDefaultNeutral;
ShowMonetPreview = !ShowNeutralPreview;
IsThemeColorEditable = normalizedMode == ThemeAppearanceValues.ColorModeSeedMonet;
IsWallpaperMode = normalizedMode == ThemeAppearanceValues.ColorModeWallpaperMonet;
PrimarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Primary);
SecondarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Secondary);
TertiarySwatchBrush = new SolidColorBrush(preview.MonetPalette.Tertiary);
NeutralSwatchBrush = new SolidColorBrush(preview.MonetPalette.Neutral);
SeedSwatchBrush = new SolidColorBrush(preview.EffectiveSeedColor);
if (IsWallpaperMode)
{
WallpaperSeedCandidates = preview.WallpaperSeedCandidates
.Select((color, index) => new ThemeSeedCandidateOption(
color.ToString(),
$"{PreviewSeedLabel} {index + 1}",
color,
string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
.ToArray();
if (WallpaperSeedCandidates.Count > 0 &&
!string.Equals(_selectedWallpaperSeed, preview.EffectiveSeedColor.ToString(), StringComparison.OrdinalIgnoreCase))
{
_selectedWallpaperSeed = preview.EffectiveSeedColor.ToString();
WallpaperSeedCandidates = preview.WallpaperSeedCandidates
.Select((color, index) => new ThemeSeedCandidateOption(
color.ToString(),
$"{PreviewSeedLabel} {index + 1}",
color,
string.Equals(color.ToString(), _selectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)))
.ToArray();
}
IsWallpaperSeedSelectable = WallpaperSeedCandidates.Count > 1;
ThemeColorSourceDescription = preview.ResolvedSeedSource switch
{
"app_wallpaper" or "app_video" or "app_solid" => ThemeSourceWallpaperAppDescription,
"system_wallpaper" => ThemeSourceWallpaperSystemDescription,
_ => ThemeSourceWallpaperFallbackDescription
};
}
else
{
WallpaperSeedCandidates = [];
IsWallpaperSeedSelectable = false;
ThemeColorSourceDescription = normalizedMode switch
{
ThemeAppearanceValues.ColorModeDefaultNeutral => ThemeSourceDefaultDescription,
_ => ThemeSourceUserColorDescription
};
}
}
private string ResolveMaterialModeLabel(string value)
{
return ThemeAppearanceValues.NormalizeSystemMaterialMode(value) switch
{
ThemeAppearanceValues.MaterialMica => SystemMaterialMicaText,
ThemeAppearanceValues.MaterialAcrylic => SystemMaterialAcrylicText,
_ => SystemMaterialNoneText
};
}
private void SyncCustomSeedPickerWithSavedThemeColor()
{
CustomSeedPickerValue = !string.IsNullOrWhiteSpace(ThemeColor) && Color.TryParse(ThemeColor, out var parsedColor)
? parsedColor
: DefaultSeedColor;
}
private IReadOnlyList<SelectionOption> CreateThemeColorModes()
{
return
[
new SelectionOption(ThemeAppearanceValues.ColorModeDefaultNeutral, ThemeSourceNeutralText),
new SelectionOption(ThemeAppearanceValues.ColorModeSeedMonet, ThemeSourceUserColorText),
new SelectionOption(ThemeAppearanceValues.ColorModeWallpaperMonet, ThemeSourceWallpaperText)
];
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed partial class ComponentsSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private bool _isInitializing;
public ComponentsSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
SpacingPresets = CreateSpacingPresets();
RefreshLocalizedText();
_isInitializing = true;
Load();
_isInitializing = false;
}
public IReadOnlyList<SelectionOption> SpacingPresets { get; }
[ObservableProperty]
private int _shortSideCells;
[ObservableProperty]
private int _edgeInsetPercent;
[ObservableProperty]
private SelectionOption _selectedSpacingPreset = new("Relaxed", "Relaxed");
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _componentsHeader = string.Empty;
[ObservableProperty]
private string _shortSideCellsLabel = string.Empty;
[ObservableProperty]
private string _edgeInsetPercentLabel = string.Empty;
[ObservableProperty]
private string _spacingPresetLabel = string.Empty;
[ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings.DefaultCornerRadiusScale;
public double GlobalCornerRadiusMinimum => GlobalAppearanceSettings.MinimumCornerRadiusScale;
public double GlobalCornerRadiusMaximum => GlobalAppearanceSettings.MaximumCornerRadiusScale;
[ObservableProperty]
private string _componentRadiusHeader = string.Empty;
[ObservableProperty]
private string _globalCornerRadiusLabel = string.Empty;
[ObservableProperty]
private string _globalCornerRadiusDescription = string.Empty;
public void Load()
{
var state = _settingsFacade.Grid.Get();
ShortSideCells = state.ShortSideCells;
EdgeInsetPercent = state.EdgeInsetPercent;
var spacingPreset = _settingsFacade.Grid.NormalizeSpacingPreset(state.SpacingPreset);
SelectedSpacingPreset = SpacingPresets.FirstOrDefault(option =>
string.Equals(option.Value, spacingPreset, StringComparison.OrdinalIgnoreCase))
?? SpacingPresets[1];
var theme = _settingsFacade.Theme.Get();
GlobalCornerRadiusScale = GlobalAppearanceSettings.NormalizeCornerRadiusScale(theme.GlobalCornerRadiusScale);
}
partial void OnShortSideCellsChanged(int value)
{
if (_isInitializing)
{
return;
}
SaveGrid();
}
partial void OnEdgeInsetPercentChanged(int value)
{
if (_isInitializing)
{
return;
}
SaveGrid();
}
partial void OnSelectedSpacingPresetChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
SaveGrid();
}
partial void OnGlobalCornerRadiusScaleChanged(double value)
{
if (_isInitializing)
{
return;
}
var normalized = GlobalAppearanceSettings.NormalizeCornerRadiusScale(value);
if (Math.Abs(normalized - value) > 0.0001d)
{
_isInitializing = true;
try
{
GlobalCornerRadiusScale = normalized;
}
finally
{
_isInitializing = false;
}
return;
}
SaveComponentCornerRadius();
}
private void SaveGrid()
{
_settingsFacade.Grid.Save(new GridSettingsState(
Math.Clamp(ShortSideCells, 6, 96),
_settingsFacade.Grid.NormalizeSpacingPreset(SelectedSpacingPreset.Value),
Math.Clamp(EdgeInsetPercent, 0, 30)));
}
private void SaveComponentCornerRadius()
{
var theme = _settingsFacade.Theme.Get();
_settingsFacade.Theme.Save(new ThemeAppearanceSettingsState(
theme.IsNightMode,
theme.ThemeColor,
theme.UseSystemChrome,
GlobalAppearanceSettings.NormalizeCornerRadiusScale(GlobalCornerRadiusScale),
theme.ThemeColorMode,
theme.SystemMaterialMode,
theme.SelectedWallpaperSeed));
}
private IReadOnlyList<SelectionOption> CreateSpacingPresets()
{
return
[
new SelectionOption("Compact", L("settings.components.spacing_compact", "Compact")),
new SelectionOption("Relaxed", L("settings.components.spacing_relaxed", "Relaxed"))
];
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.components.title", "Components");
PageDescription = L("settings.components.description", "Adjust component layout and corner design.");
ComponentsHeader = L("settings.components.header", "Grid Settings");
ShortSideCellsLabel = L("settings.components.short_side_label", "Short Side Cells");
EdgeInsetPercentLabel = L("settings.components.edge_inset_label", "Screen Inset");
SpacingPresetLabel = L("settings.components.spacing_label", "Component Spacing");
ComponentRadiusHeader = L("settings.components.corner_radius.header", "Corner Design");
GlobalCornerRadiusLabel = L("settings.components.corner_radius.label", "Component Corner Radius");
GlobalCornerRadiusDescription = L(
"settings.components.corner_radius.description",
"Adjust the shared corner radius from a square edge to a capsule-like shape, and expand the internal safe area with it.");
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed partial class InstalledPluginItemViewModel : ViewModelBase
{
public InstalledPluginItemViewModel(InstalledPluginInfo info)
{
PluginId = info.Manifest.Id;
Name = info.Manifest.Name;
Version = info.Manifest.Version ?? "-";
Description = info.Manifest.Description;
ErrorMessage = info.ErrorMessage;
IsLoaded = info.IsLoaded;
IsPackage = info.IsPackage;
IsEnabled = info.IsEnabled;
}
public string PluginId { get; }
public string Name { get; }
public string Version { get; }
public string? Description { get; }
public string? ErrorMessage { get; }
public bool IsLoaded { get; }
public bool IsPackage { get; }
[ObservableProperty]
private bool _isEnabled;
}
public sealed partial class PluginsSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
public PluginsSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
StatusMessage = L(
"settings.plugins.initial_status",
"Refresh plugin state to see the latest installed and marketplace entries.");
}
public event Action? RestartRequested;
public ObservableCollection<InstalledPluginItemViewModel> InstalledPlugins { get; } = [];
[ObservableProperty]
private string _statusMessage = string.Empty;
[ObservableProperty]
private bool _isBusy;
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _refreshButtonText = string.Empty;
[ObservableProperty]
private string _installedHeader = string.Empty;
[ObservableProperty]
private string _deleteButtonText = string.Empty;
[ObservableProperty]
private string _emptyInstalledText = string.Empty;
[ObservableProperty]
private string _restartRequiredMessage = string.Empty;
public async Task InitializeAsync()
{
if (InstalledPlugins.Count > 0)
{
return;
}
await RefreshAsync();
}
[RelayCommand]
private async Task RefreshAsync()
{
if (IsBusy)
{
return;
}
try
{
IsBusy = true;
InstalledPlugins.Clear();
foreach (var plugin in _settingsFacade.PluginManagement.GetInstalledPlugins())
{
InstalledPlugins.Add(new InstalledPluginItemViewModel(plugin));
}
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L(
"settings.plugins.refresh_success_installed_format",
"Loaded {0} installed plugins."),
InstalledPlugins.Count);
}
finally
{
IsBusy = false;
}
}
[RelayCommand]
private void TogglePlugin(InstalledPluginItemViewModel? item)
{
if (item is null)
{
return;
}
if (_settingsFacade.PluginManagement.SetPluginEnabled(item.PluginId, item.IsEnabled))
{
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L(
"settings.plugins.toggle_result_format",
"Plugin '{0}' was {1} for the next launch. Restart the app to apply page and widget changes."),
item.Name,
item.IsEnabled
? L("settings.plugins.toggle_state_enabled", "enabled")
: L("settings.plugins.toggle_state_disabled", "disabled"));
RestartRequested?.Invoke();
}
else
{
item.IsEnabled = !item.IsEnabled;
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L("settings.plugins.toggle_unchanged_format", "Plugin '{0}' did not change."),
item.Name);
}
}
[RelayCommand]
private void DeletePlugin(InstalledPluginItemViewModel? item)
{
if (item is null)
{
return;
}
if (_settingsFacade.PluginManagement.DeleteInstalledPlugin(item.PluginId))
{
InstalledPlugins.Remove(item);
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L(
"settings.plugins.delete_success_format",
"Plugin '{0}' was staged for deletion. Restart the app to finish removing it."),
item.Name);
RestartRequested?.Invoke();
}
else
{
StatusMessage = string.Format(
CultureInfo.CurrentCulture,
L("settings.plugins.delete_failed_name_format", "Failed to remove plugin '{0}'."),
item.Name);
}
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.plugins.title", "Plugins");
PageDescription = L("settings.plugins.description", "Manage installed plugins and review their runtime state.");
RefreshButtonText = L("settings.plugins.refresh_button", "Refresh Plugins");
InstalledHeader = L("settings.plugins.installed_header", "Installed Plugins");
DeleteButtonText = L("settings.plugins.delete_button_short", "Delete");
EmptyInstalledText = L("settings.plugins.empty", "No plugins found.");
RestartRequiredMessage = L("settings.plugins.restart_required", "Plugin changes take effect after restart.");
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed partial class AboutSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
public AboutSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
VersionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
CodenameText = _settingsFacade.ApplicationInfo.GetAppCodenameText();
var backendInfo = _settingsFacade.ApplicationInfo.GetRenderBackendInfo();
RenderBackendText = string.IsNullOrWhiteSpace(backendInfo.ImplementationTypeName)
? backendInfo.ActualBackend
: $"{backendInfo.ActualBackend} ({backendInfo.ImplementationTypeName})";
}
[ObservableProperty]
private string _versionText = "-";
[ObservableProperty]
private string _codenameText = "-";
[ObservableProperty]
private string _renderBackendText = "-";
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _appInfoHeader = string.Empty;
[ObservableProperty]
private string _versionLabel = string.Empty;
[ObservableProperty]
private string _codenameLabel = string.Empty;
[ObservableProperty]
private string _renderBackendLabel = string.Empty;
private void RefreshLocalizedText()
{
PageTitle = L("settings.about.title", "About");
PageDescription = L("settings.about.description", "Application details.");
AppInfoHeader = L("settings.about.app_info_header", "Application Information");
VersionLabel = L("settings.about.version_label", "Version");
CodenameLabel = L("settings.about.codename_label", "Codename");
RenderBackendLabel = L("settings.about.render_backend_label", "Render Backend");
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly UpdateWorkflowService _updateWorkflowService;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
private readonly Version _currentVersion;
private bool _isInitializing;
private UpdateCheckResult? _lastCheckResult;
public IReadOnlyList<SelectionOption> UpdateChannelOptions { get; }
public IReadOnlyList<SelectionOption> UpdateSourceOptions { get; }
public IReadOnlyList<SelectionOption> UpdateModeOptions { get; }
public IReadOnlyList<SelectionOption> DownloadThreadOptions { get; }
public UpdateSettingsPageViewModel(
ISettingsFacadeService settingsFacade,
UpdateWorkflowService? updateWorkflowService = null)
{
_settingsFacade = settingsFacade;
_updateWorkflowService = updateWorkflowService ?? HostUpdateWorkflowServiceProvider.GetOrCreate();
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
UpdateChannelOptions = CreateUpdateChannelOptions();
UpdateSourceOptions = CreateUpdateSourceOptions();
UpdateModeOptions = CreateUpdateModeOptions();
DownloadThreadOptions = CreateDownloadThreadOptions();
var versionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
_currentVersion = Version.TryParse(versionText, out var parsedVersion)
? parsedVersion
: new Version(0, 0, 0);
CurrentVersionText = versionText;
LoadStateFromSettings();
}
[ObservableProperty]
private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
[ObservableProperty]
private string _selectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
[ObservableProperty]
private string _selectedUpdateModeValue = UpdateSettingsValues.ModeDownloadThenConfirm;
[ObservableProperty]
private string _currentVersionText = "-";
[ObservableProperty]
private string _updateStatus = string.Empty;
[ObservableProperty]
private bool _isCheckingForUpdates;
[ObservableProperty]
private bool _isDownloading;
[ObservableProperty]
private double _downloadProgressValue;
[ObservableProperty]
private bool _isDownloadProgressVisible;
[ObservableProperty]
private string _downloadProgressText = string.Empty;
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
[ObservableProperty]
private string _statusCardTitle = string.Empty;
[ObservableProperty]
private string _statusCardDescription = string.Empty;
[ObservableProperty]
private string _preferencesHeader = string.Empty;
[ObservableProperty]
private string _preferencesDescription = string.Empty;
[ObservableProperty]
private string _updateChannelLabel = string.Empty;
[ObservableProperty]
private string _updateSourceLabel = string.Empty;
[ObservableProperty]
private string _updateModeLabel = string.Empty;
[ObservableProperty]
private string _currentVersionLabel = string.Empty;
[ObservableProperty]
private string _latestVersionLabel = string.Empty;
[ObservableProperty]
private string _publishedAtLabel = string.Empty;
[ObservableProperty]
private string _lastCheckedLabel = string.Empty;
[ObservableProperty]
private string _checkForUpdatesButtonText = string.Empty;
[ObservableProperty]
private string _downloadButtonText = string.Empty;
[ObservableProperty]
private string _installNowButtonText = string.Empty;
[ObservableProperty]
private string _latestVersionText = string.Empty;
[ObservableProperty]
private string _publishedAtText = string.Empty;
[ObservableProperty]
private string _lastCheckedText = string.Empty;
[ObservableProperty]
private bool _isLatestVersionVisible;
[ObservableProperty]
private bool _isPublishedAtVisible;
[ObservableProperty]
private bool _isLastCheckedVisible;
[ObservableProperty]
private bool _hasPendingInstaller;
[ObservableProperty]
private double _downloadThreadsSliderValue = UpdateSettingsValues.DefaultDownloadThreads;
[ObservableProperty]
private string _selectedUpdateChannelDescription = string.Empty;
[ObservableProperty]
private string _selectedUpdateModeDescription = string.Empty;
[ObservableProperty]
private string _selectedUpdateSourceDescription = string.Empty;
[ObservableProperty]
private string _downloadThreadsLabel = string.Empty;
[ObservableProperty]
private string _downloadThreadsDescription = string.Empty;
[ObservableProperty]
private string _stableChannelText = string.Empty;
[ObservableProperty]
private string _previewChannelText = string.Empty;
[ObservableProperty]
private string _gitHubSourceText = string.Empty;
[ObservableProperty]
private string _ghProxySourceText = string.Empty;
[ObservableProperty]
private string _manualModeText = string.Empty;
[ObservableProperty]
private string _downloadThenConfirmModeText = string.Empty;
[ObservableProperty]
private string _silentOnExitModeText = string.Empty;
[ObservableProperty]
private SelectionOption? _selectedUpdateChannelOption;
[ObservableProperty]
private SelectionOption? _selectedUpdateSourceOption;
[ObservableProperty]
private SelectionOption? _selectedUpdateModeOption;
[ObservableProperty]
private SelectionOption? _selectedDownloadThreadsOption;
[ObservableProperty]
private string _downloadThreadsText = UpdateSettingsValues.DefaultDownloadThreads.ToString(CultureInfo.CurrentCulture);
public bool IsStableChannelSelected =>
string.Equals(SelectedUpdateChannelValue, UpdateSettingsValues.ChannelStable, StringComparison.OrdinalIgnoreCase);
public bool IsPreviewChannelSelected =>
string.Equals(SelectedUpdateChannelValue, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase);
public bool IsGitHubSourceSelected =>
string.Equals(SelectedUpdateSourceValue, UpdateSettingsValues.DownloadSourceGitHub, StringComparison.OrdinalIgnoreCase);
public bool IsGhProxySourceSelected =>
string.Equals(SelectedUpdateSourceValue, UpdateSettingsValues.DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase);
public bool IsManualModeSelected =>
string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase);
public bool IsDownloadThenConfirmModeSelected =>
string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase);
public bool IsSilentOnExitModeSelected =>
string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase);
public bool IsDownloadButtonVisible =>
!HasPendingInstaller &&
_lastCheckResult is { Success: true, IsUpdateAvailable: true, PreferredAsset: not null };
public bool IsInstallButtonVisible => HasPendingInstaller;
public string DownloadThreadsValueText =>
UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.CurrentCulture);
private bool IsBusy => IsCheckingForUpdates || IsDownloading;
partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
{
if (value is not null &&
!string.Equals(SelectedUpdateChannelValue, value.Value, StringComparison.OrdinalIgnoreCase))
{
SelectedUpdateChannelValue = value.Value;
}
}
partial void OnSelectedUpdateSourceOptionChanged(SelectionOption? value)
{
if (value is not null &&
!string.Equals(SelectedUpdateSourceValue, value.Value, StringComparison.OrdinalIgnoreCase))
{
SelectedUpdateSourceValue = value.Value;
}
}
partial void OnSelectedUpdateModeOptionChanged(SelectionOption? value)
{
if (value is not null &&
!string.Equals(SelectedUpdateModeValue, value.Value, StringComparison.OrdinalIgnoreCase))
{
SelectedUpdateModeValue = value.Value;
}
}
partial void OnSelectedDownloadThreadsOptionChanged(SelectionOption? value)
{
if (value is null || !int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
{
return;
}
ApplyDownloadThreadsValue(parsed, !_isInitializing);
}
partial void OnSelectedUpdateChannelValueChanged(string value)
{
if (_isInitializing)
{
return;
}
_lastCheckResult = null;
if (!HasPendingInstaller)
{
LatestVersionText = string.Empty;
PublishedAtText = string.Empty;
IsLatestVersionVisible = false;
IsPublishedAtVisible = false;
}
SaveUpdateSettings();
UpdateStatus = string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_channel_changed_format", "Update channel switched to {0}. Please check again."),
string.Equals(value, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase)
? L("settings.update.channel_preview", "Preview")
: L("settings.update.channel_stable", "Stable"));
SelectedUpdateChannelDescription = BuildUpdateChannelDescription(value);
SyncSelectedOptions();
RefreshActionState();
}
partial void OnSelectedUpdateSourceValueChanged(string value)
{
if (_isInitializing)
{
return;
}
SaveUpdateSettings();
SelectedUpdateSourceDescription = BuildUpdateSourceDescription(value);
UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
SyncSelectedOptions();
}
partial void OnSelectedUpdateModeValueChanged(string value)
{
if (_isInitializing)
{
return;
}
SaveUpdateSettings();
SelectedUpdateModeDescription = BuildUpdateModeDescription(value);
UpdateStatus = HasPendingInstaller
? BuildPendingReadyStatus()
: L("settings.update.status_preferences_saved", "Update preferences saved.");
SyncSelectedOptions();
RefreshActionState();
}
partial void OnDownloadThreadsSliderValueChanged(double value)
{
var normalized = UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(value));
if (Math.Abs(value - normalized) > double.Epsilon)
{
DownloadThreadsSliderValue = normalized;
return;
}
OnPropertyChanged(nameof(DownloadThreadsValueText));
if (_isInitializing)
{
return;
}
SaveUpdateSettings();
UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
SyncSelectedOptions();
}
partial void OnDownloadThreadsTextChanged(string value)
{
if (_isInitializing)
{
return;
}
if (!TryParseDownloadThreads(value, out var parsed))
{
return;
}
ApplyDownloadThreadsValue(parsed, true);
}
partial void OnHasPendingInstallerChanged(bool value)
{
RefreshActionState();
if (!value)
{
UpdateStatus = L("settings.update.status_ready", "Ready to check for updates.");
}
}
partial void OnIsCheckingForUpdatesChanged(bool value)
{
CheckForUpdatesCommand.NotifyCanExecuteChanged();
DownloadLatestReleaseCommand.NotifyCanExecuteChanged();
InstallPendingUpdateCommand.NotifyCanExecuteChanged();
}
partial void OnIsDownloadingChanged(bool value)
{
CheckForUpdatesCommand.NotifyCanExecuteChanged();
DownloadLatestReleaseCommand.NotifyCanExecuteChanged();
InstallPendingUpdateCommand.NotifyCanExecuteChanged();
}
[RelayCommand]
private void SelectStableChannel()
{
SelectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
}
[RelayCommand]
private void SelectPreviewChannel()
{
SelectedUpdateChannelValue = UpdateSettingsValues.ChannelPreview;
}
[RelayCommand]
private void SelectGitHubSource()
{
SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
}
[RelayCommand]
private void SelectGhProxySource()
{
SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGhProxy;
}
[RelayCommand]
private void SelectManualMode()
{
SelectedUpdateModeValue = UpdateSettingsValues.ModeManual;
}
[RelayCommand]
private void SelectDownloadThenConfirmMode()
{
SelectedUpdateModeValue = UpdateSettingsValues.ModeDownloadThenConfirm;
}
[RelayCommand]
private void SelectSilentOnExitMode()
{
SelectedUpdateModeValue = UpdateSettingsValues.ModeSilentOnExit;
}
private void SaveUpdateSettings()
{
var current = _settingsFacade.Update.Get();
_settingsFacade.Update.Save(current with
{
IncludePrereleaseUpdates = string.Equals(
SelectedUpdateChannelValue,
UpdateSettingsValues.ChannelPreview,
StringComparison.OrdinalIgnoreCase),
UpdateChannel = SelectedUpdateChannelValue,
UpdateMode = SelectedUpdateModeValue,
UpdateDownloadSource = SelectedUpdateSourceValue,
UpdateDownloadThreads = UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue))
});
}
private bool CanCheckForUpdates() => !IsBusy;
[RelayCommand(CanExecute = nameof(CanCheckForUpdates))]
private async Task CheckForUpdatesAsync()
{
try
{
IsCheckingForUpdates = true;
IsDownloadProgressVisible = false;
DownloadProgressValue = 0;
DownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
UpdateStatus = L("settings.update.status_checking", "Checking GitHub releases...");
var result = await _updateWorkflowService.CheckForUpdatesAsync(_currentVersion);
_lastCheckResult = result.Success ? result : null;
RefreshLastCheckedFromSettings();
if (!result.Success)
{
UpdateStatus = string.IsNullOrWhiteSpace(result.ErrorMessage)
? L("settings.update.status_check_failed", "Failed to check for updates.")
: string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_check_failed_format", "Update check failed: {0}"),
result.ErrorMessage);
return;
}
ApplyCheckResultDisplay(result);
if (!result.IsUpdateAvailable)
{
return;
}
if (result.PreferredAsset is null)
{
UpdateStatus = L(
"settings.update.status_asset_missing",
"A new release is available, but no compatible installer was found.");
return;
}
if (!string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
{
await DownloadLatestReleaseCoreAsync(result, invokedFromCheck: true);
return;
}
UpdateStatus = string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_available_format", "New version {0} is available. Click Download & Install."),
result.LatestVersionText);
}
finally
{
IsCheckingForUpdates = false;
}
}
private bool CanDownloadLatestRelease() => !IsBusy && IsDownloadButtonVisible;
[RelayCommand(CanExecute = nameof(CanDownloadLatestRelease))]
private async Task DownloadLatestReleaseAsync()
{
await DownloadLatestReleaseCoreAsync(_lastCheckResult, invokedFromCheck: false);
}
private bool CanInstallPendingUpdate() => !IsBusy && HasPendingInstaller;
[RelayCommand(CanExecute = nameof(CanInstallPendingUpdate))]
private void InstallPendingUpdate()
{
var result = _updateWorkflowService.LaunchPendingInstallerNow();
if (result.Success)
{
UpdateStatus = L(
"settings.update.status_installer_started",
"Installer started. The app will close for update.");
HasPendingInstaller = false;
return;
}
UpdateStatus = result.UserCancelledElevation
? L(
"settings.update.status_elevation_cancelled",
"Administrator permission was not granted. Update was cancelled.")
: string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_launch_failed_format", "Failed to start installer: {0}"),
result.ErrorMessage ?? L("settings.update.status_installer_missing", "Installer file was not found after download."));
}
private void RefreshLocalizedText()
{
PageTitle = L("settings.update.title", "Update");
PageDescription = L("settings.update.description", "Update checks and release channel preferences.");
StatusCardTitle = L("settings.update.status_card_title", "Update Status");
StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information.");
PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed.");
UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
UpdateSourceLabel = L("settings.update.source_label", "Download Source");
UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
DownloadThreadsLabel = L("settings.update.download_threads_label", "Download Threads");
DownloadThreadsDescription = L("settings.update.download_threads_desc", "Choose how many parallel download threads are used for application updates.");
CheckForUpdatesButtonText = L("settings.update.check_button", "Check for Updates");
DownloadButtonText = L("settings.update.download_install_button", "Download & Install");
InstallNowButtonText = L("settings.update.install_now_button", "Install Now");
CurrentVersionLabel = L("settings.update.current_version_label", "Current Version");
LatestVersionLabel = L("settings.update.latest_version_label", "Latest Release");
PublishedAtLabel = L("settings.update.published_at_label", "Published At");
LastCheckedLabel = L("settings.update.last_checked_label", "Last Checked");
StableChannelText = L("settings.update.channel_stable", "Stable");
PreviewChannelText = L("settings.update.channel_preview", "Preview");
GitHubSourceText = L("settings.update.source_github", "GitHub");
GhProxySourceText = L("settings.update.source_ghproxy", "gh-proxy");
ManualModeText = L("settings.update.mode_manual", "Manual Update");
DownloadThenConfirmModeText = L("settings.update.mode_download_then_confirm", "Silent Download");
SilentOnExitModeText = L("settings.update.mode_silent_on_exit", "Silent Install");
SelectedUpdateChannelDescription = BuildUpdateChannelDescription(SelectedUpdateChannelValue);
SelectedUpdateModeDescription = BuildUpdateModeDescription(SelectedUpdateModeValue);
SelectedUpdateSourceDescription = BuildUpdateSourceDescription(SelectedUpdateSourceValue);
}
private void LoadStateFromSettings()
{
var update = _settingsFacade.Update.Get();
_isInitializing = true;
SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);
DownloadThreadsSliderValue = UpdateSettingsValues.NormalizeDownloadThreads(update.UpdateDownloadThreads);
DownloadThreadsText = ((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.CurrentCulture);
_isInitializing = false;
SyncSelectedOptions();
RefreshLastCheckedFromSettings();
ApplyPendingState(update);
if (!HasPendingInstaller)
{
UpdateStatus = L("settings.update.status_idle", "No update check has been performed yet.");
}
RefreshActionState();
}
private void RefreshLastCheckedFromSettings()
{
var update = _settingsFacade.Update.Get();
LastCheckedText = FormatTimestamp(update.LastUpdateCheckUtcMs);
IsLastCheckedVisible = !string.IsNullOrWhiteSpace(LastCheckedText);
}
private void ApplyPendingState(UpdateSettingsState update)
{
var pending = _updateWorkflowService.GetPendingUpdate();
HasPendingInstaller = pending is not null;
if (pending is null)
{
return;
}
LatestVersionText = pending.VersionText;
IsLatestVersionVisible = !string.IsNullOrWhiteSpace(LatestVersionText);
PublishedAtText = pending.PublishedAt is null ? string.Empty : FormatTimestamp(pending.PublishedAt.Value.ToUnixTimeMilliseconds());
IsPublishedAtVisible = !string.IsNullOrWhiteSpace(PublishedAtText);
UpdateStatus = BuildPendingReadyStatus();
}
private void ApplyCheckResultDisplay(UpdateCheckResult result)
{
if (result.IsUpdateAvailable)
{
LatestVersionText = result.LatestVersionText;
IsLatestVersionVisible = !string.IsNullOrWhiteSpace(LatestVersionText);
PublishedAtText = result.Release is null || result.Release.PublishedAt == DateTimeOffset.MinValue
? string.Empty
: FormatTimestamp(result.Release.PublishedAt.ToUnixTimeMilliseconds());
IsPublishedAtVisible = !string.IsNullOrWhiteSpace(PublishedAtText);
return;
}
LatestVersionText = string.Empty;
PublishedAtText = string.Empty;
IsLatestVersionVisible = false;
IsPublishedAtVisible = false;
UpdateStatus = string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_up_to_date_format", "You are up to date ({0})."),
result.CurrentVersionText);
}
private async Task DownloadLatestReleaseCoreAsync(UpdateCheckResult? result, bool invokedFromCheck)
{
if (result is null || !result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
{
return;
}
try
{
IsDownloading = true;
IsDownloadProgressVisible = true;
DownloadProgressValue = 0;
DownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
UpdateStatus = L("settings.update.status_downloading", "Downloading installer...");
var progress = new Progress<double>(value =>
{
DownloadProgressValue = Math.Clamp(value * 100d, 0d, 100d);
DownloadProgressText = string.Format(
CultureInfo.CurrentCulture,
L("settings.update.download_progress_format", "Download progress: {0:F0}%"),
DownloadProgressValue);
});
var downloadResult = await _updateWorkflowService.DownloadReleaseAsync(result, progress);
if (!downloadResult.Success)
{
UpdateStatus = string.Format(
CultureInfo.CurrentCulture,
L("settings.update.status_download_failed_format", "Download failed: {0}"),
downloadResult.ErrorMessage ?? L("settings.update.status_check_failed", "Failed to check for updates."));
return;
}
ApplyPendingState(_settingsFacade.Update.Get());
UpdateStatus = BuildPendingReadyStatus();
if (!invokedFromCheck)
{
_lastCheckResult = result;
}
}
finally
{
IsDownloading = false;
IsDownloadProgressVisible = false;
}
}
private string BuildPendingReadyStatus()
{
return string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase)
? L("settings.update.status_downloaded_exit", "Update downloaded. It will be installed when you exit the app.")
: L("settings.update.status_downloaded_confirm", "Update downloaded. Review it and choose when to install.");
}
private string BuildUpdateModeDescription(string? value)
{
return UpdateSettingsValues.NormalizeMode(value) switch
{
UpdateSettingsValues.ModeManual => L(
"settings.update.mode_manual_desc",
"Only check for updates. You decide when downloads and installation happen."),
UpdateSettingsValues.ModeSilentOnExit => L(
"settings.update.mode_silent_on_exit_desc",
"Download updates in the background and install them the next time you exit the app."),
_ => L(
"settings.update.mode_download_then_confirm_desc",
"Download updates in the background and ask for confirmation before installing them.")
};
}
private string BuildUpdateChannelDescription(string? value)
{
return UpdateSettingsValues.NormalizeChannel(value) switch
{
UpdateSettingsValues.ChannelPreview => L(
"settings.update.channel_preview_desc",
"Preview builds may contain newer features but can be less stable."),
_ => L(
"settings.update.channel_stable_desc",
"Stable builds prioritize reliability and are recommended for most users.")
};
}
private string BuildUpdateSourceDescription(string? value)
{
return UpdateSettingsValues.NormalizeDownloadSource(value) switch
{
UpdateSettingsValues.DownloadSourceGhProxy => L(
"settings.update.source_ghproxy_desc",
"Use the gh-proxy mirror when downloading GitHub release assets."),
_ => L(
"settings.update.source_github_desc",
"Download release assets directly from GitHub.")
};
}
private string FormatTimestamp(long? utcMs)
{
if (utcMs is not > 0)
{
return string.Empty;
}
try
{
return DateTimeOffset
.FromUnixTimeMilliseconds(utcMs.Value)
.ToLocalTime()
.ToString("g", CultureInfo.CurrentCulture);
}
catch (ArgumentOutOfRangeException)
{
return string.Empty;
}
}
private void RefreshActionState()
{
OnPropertyChanged(nameof(IsDownloadButtonVisible));
OnPropertyChanged(nameof(IsInstallButtonVisible));
OnPropertyChanged(nameof(DownloadThreadsValueText));
}
private IReadOnlyList<SelectionOption> CreateUpdateChannelOptions()
{
return
[
new SelectionOption(UpdateSettingsValues.ChannelStable, StableChannelText),
new SelectionOption(UpdateSettingsValues.ChannelPreview, PreviewChannelText)
];
}
private IReadOnlyList<SelectionOption> CreateUpdateSourceOptions()
{
return
[
new SelectionOption(UpdateSettingsValues.DownloadSourceGitHub, GitHubSourceText),
new SelectionOption(UpdateSettingsValues.DownloadSourceGhProxy, GhProxySourceText)
];
}
private IReadOnlyList<SelectionOption> CreateUpdateModeOptions()
{
return
[
new SelectionOption(UpdateSettingsValues.ModeManual, ManualModeText),
new SelectionOption(UpdateSettingsValues.ModeDownloadThenConfirm, DownloadThenConfirmModeText),
new SelectionOption(UpdateSettingsValues.ModeSilentOnExit, SilentOnExitModeText)
];
}
private IReadOnlyList<SelectionOption> CreateDownloadThreadOptions()
{
return Enumerable
.Range(UpdateSettingsValues.MinDownloadThreads, UpdateSettingsValues.MaxDownloadThreads)
.Select(value => new SelectionOption(
value.ToString(CultureInfo.InvariantCulture),
value.ToString(CultureInfo.CurrentCulture)))
.ToList();
}
private void SyncSelectedOptions()
{
SelectedUpdateChannelOption = UpdateChannelOptions.FirstOrDefault(option =>
string.Equals(option.Value, SelectedUpdateChannelValue, StringComparison.OrdinalIgnoreCase));
SelectedUpdateSourceOption = UpdateSourceOptions.FirstOrDefault(option =>
string.Equals(option.Value, SelectedUpdateSourceValue, StringComparison.OrdinalIgnoreCase));
SelectedUpdateModeOption = UpdateModeOptions.FirstOrDefault(option =>
string.Equals(option.Value, SelectedUpdateModeValue, StringComparison.OrdinalIgnoreCase));
SelectedDownloadThreadsOption = DownloadThreadOptions.FirstOrDefault(option =>
string.Equals(
option.Value,
UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.InvariantCulture),
StringComparison.OrdinalIgnoreCase));
}
private void ApplyDownloadThreadsValue(int value, bool saveChanges)
{
var normalized = UpdateSettingsValues.NormalizeDownloadThreads(value);
var normalizedText = normalized.ToString(CultureInfo.CurrentCulture);
var previousInitializing = _isInitializing;
_isInitializing = true;
DownloadThreadsSliderValue = normalized;
DownloadThreadsText = normalizedText;
_isInitializing = previousInitializing;
SyncSelectedOptions();
if (saveChanges)
{
SaveUpdateSettings();
UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
}
}
private static bool TryParseDownloadThreads(string? value, out int parsed)
{
if (int.TryParse(value, NumberStyles.Integer, CultureInfo.CurrentCulture, out parsed))
{
return true;
}
return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed);
}
private string L(string key, string fallback)
=> _localizationService.GetString(_languageCode, key, fallback);
}
public sealed class PluginGeneratedSettingsPageViewModel
{
public PluginGeneratedSettingsPageViewModel(
ISettingsService settingsService,
string pluginId,
PluginSettingsSectionRegistration section,
PluginLocalizer localizer)
{
SettingsService = settingsService;
PluginId = pluginId;
Section = section;
Localizer = localizer;
Title = localizer.GetString(section.TitleLocalizationKey, section.TitleLocalizationKey);
Description = string.IsNullOrWhiteSpace(section.DescriptionLocalizationKey)
? null
: localizer.GetString(section.DescriptionLocalizationKey, section.DescriptionLocalizationKey);
}
public ISettingsService SettingsService { get; }
public string PluginId { get; }
public PluginSettingsSectionRegistration Section { get; }
public PluginLocalizer Localizer { get; }
public string Title { get; }
public string? Description { get; }
}