2026-03-13 22:20:12 +08:00
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 ;
2026-03-20 00:41:14 +08:00
using LanMountainDesktop.Settings.Core ;
2026-03-13 22:20:12 +08:00
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 ;
2026-03-14 13:36:18 +08:00
[ObservableProperty]
private bool _isPageTitleVisible = true ;
2026-03-13 22:20:12 +08:00
[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 ;
2026-03-15 17:08:07 +08:00
[ObservableProperty]
private string _restartDialogTitle = string . Empty ;
[ObservableProperty]
private string _restartDialogPrimaryText = string . Empty ;
[ObservableProperty]
private string _restartDialogCloseText = string . Empty ;
2026-03-13 22:20:12 +08:00
[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" ) ;
2026-03-15 17:08:07 +08:00
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" ) ) ;
2026-03-13 22:20:12 +08:00
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 ; }
}
2026-03-15 17:08:07 +08:00
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 ; }
}
2026-03-13 22:20:12 +08:00
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 ;
}
2026-03-17 12:30:30 +08:00
// 更新语言代码并刷新UI文本
_languageCode = _localizationService . NormalizeLanguageCode ( value . Value ) ;
RefreshLocalizedText ( ) ;
RefreshPreview ( ) ;
// 保存设置
2026-03-13 22:20:12 +08:00
_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" , "中文" ) ) ,
2026-03-22 14:47:15 +08:00
new SelectionOption ( "en-US" , L ( "settings.region.language_en" , "English" ) ) ,
2026-03-22 23:30:43 +08:00
new SelectionOption ( "ja-JP" , L ( "settings.region.language_ja" , "日本語" ) ) ,
new SelectionOption ( "ko-KR" , L ( "settings.region.language_ko" , "한국어" ) )
2026-03-13 22:20:12 +08:00
] ;
}
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
{
2026-03-15 17:08:07 +08:00
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" ) ) ;
2026-03-13 22:20:12 +08:00
private readonly ISettingsFacadeService _settingsFacade ;
2026-03-15 17:08:07 +08:00
private readonly IAppearanceThemeService _appearanceThemeService ;
2026-03-13 22:20:12 +08:00
private readonly LocalizationService _localizationService = new ( ) ;
private readonly string _languageCode ;
private bool _isInitializing ;
2026-03-15 17:08:07 +08:00
private string? _selectedWallpaperSeed ;
2026-03-13 22:20:12 +08:00
2026-03-15 17:08:07 +08:00
public AppearanceSettingsPageViewModel (
ISettingsFacadeService settingsFacade ,
IAppearanceThemeService appearanceThemeService )
2026-03-13 22:20:12 +08:00
{
_settingsFacade = settingsFacade ;
2026-03-15 17:08:07 +08:00
_appearanceThemeService = appearanceThemeService ;
2026-03-13 22:20:12 +08:00
_languageCode = _localizationService . NormalizeLanguageCode ( _settingsFacade . Region . Get ( ) . LanguageCode ) ;
RefreshLocalizedText ( ) ;
2026-03-15 17:08:07 +08:00
ThemeColorModes = CreateThemeColorModes ( ) ;
2026-03-13 22:20:12 +08:00
_isInitializing = true ;
Load ( ) ;
_isInitializing = false ;
}
2026-03-15 17:08:07 +08:00
public event Action < string > ? RestartRequested ;
2026-03-13 22:20:12 +08:00
2026-03-15 17:08:07 +08:00
public IReadOnlyList < SelectionOption > ThemeColorModes { get ; }
[ObservableProperty]
private IReadOnlyList < SelectionOption > _systemMaterialModes = [ ] ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private bool _isNightMode ;
[ObservableProperty]
private string _themeColor = string . Empty ;
2026-03-14 16:38:56 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private Color _customSeedPickerValue = DefaultSeedColor ;
2026-03-14 16:38:56 +08:00
2026-03-15 17:08:07 +08:00
partial void OnCustomSeedPickerValueChanged ( Color value )
2026-03-14 16:38:56 +08:00
{
2026-03-15 17:08:07 +08:00
if ( _isInitializing | |
! string . Equals ( SelectedThemeColorMode ? . Value , ThemeAppearanceValues . ColorModeSeedMonet , StringComparison . OrdinalIgnoreCase ) )
2026-03-14 16:38:56 +08:00
{
return ;
}
2026-03-15 17:08:07 +08:00
UpdatePreview ( BuildPendingState ( usePickerSeed : true ) ) ;
2026-03-14 16:38:56 +08:00
}
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private bool _useSystemChrome ;
2026-03-20 00:41:14 +08:00
[ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings . DefaultCornerRadiusScale ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
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 ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _themeColorSourceDescription = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _systemMaterialDescription = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
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 = [ ] ;
2026-03-13 22:20:12 +08:00
[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 ;
2026-03-15 17:08:07 +08:00
[ObservableProperty]
private string _themeColorModeLabel = string . Empty ;
[ObservableProperty]
private string _systemMaterialLabel = string . Empty ;
2026-03-20 00:41:14 +08:00
[ObservableProperty]
private string _globalCornerRadiusLabel = string . Empty ;
[ObservableProperty]
private string _globalCornerRadiusDescription = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _themeHeader = string . Empty ;
[ObservableProperty]
2026-03-15 17:08:07 +08:00
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 ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewSecondaryLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewTertiaryLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewNeutralLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewSeedLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewNeutralLightLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
private string _previewNeutralDarkLabel = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 17:08:07 +08:00
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 ;
2026-03-13 22:20:12 +08:00
public void Load ( )
{
var theme = _settingsFacade . Theme . Get ( ) ;
2026-03-15 17:08:07 +08:00
var liveSnapshot = _appearanceThemeService . GetCurrent ( ) ;
RefreshMaterialModeOptions ( liveSnapshot ) ;
_isInitializing = true ;
try
2026-03-14 16:38:56 +08:00
{
2026-03-15 17:08:07 +08:00
ApplySavedState ( theme ) ;
2026-03-14 16:38:56 +08:00
}
2026-03-15 17:08:07 +08:00
finally
2026-03-14 16:38:56 +08:00
{
2026-03-15 17:08:07 +08:00
_isInitializing = false ;
2026-03-14 16:38:56 +08:00
}
2026-03-13 22:20:12 +08:00
2026-03-15 17:08:07 +08:00
UpdatePreview ( theme ) ;
2026-03-13 22:20:12 +08:00
}
partial void OnIsNightModeChanged ( bool value )
{
if ( _isInitializing )
{
return ;
}
2026-03-15 17:08:07 +08:00
PersistCurrentState ( restartRequired : false ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
partial void OnUseSystemChromeChanged ( bool value )
2026-03-13 22:20:12 +08:00
{
if ( _isInitializing )
{
return ;
}
2026-03-15 17:08:07 +08:00
PersistCurrentState ( restartRequired : false ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-20 00:41:14 +08:00
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 ) ;
}
2026-03-15 17:08:07 +08:00
partial void OnSelectedThemeColorModeChanged ( SelectionOption value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
if ( _isInitializing | | value is null )
2026-03-13 22:20:12 +08:00
{
return ;
}
2026-03-15 17:08:07 +08:00
PersistCurrentState ( restartRequired : true ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
partial void OnSelectedSystemMaterialModeChanged ( SelectionOption value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
if ( _isInitializing | | value is null )
2026-03-13 22:20:12 +08:00
{
return ;
}
2026-03-15 17:08:07 +08:00
PersistCurrentState ( restartRequired : true ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
[RelayCommand]
private void ApplyCustomSeed ( )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
if ( ! IsThemeColorEditable )
2026-03-13 22:20:12 +08:00
{
return ;
}
2026-03-15 17:08:07 +08:00
ThemeColor = CustomSeedPickerValue . ToString ( ) ;
PersistCurrentState ( restartRequired : false ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
public void CancelCustomSeedPreview ( )
2026-03-13 22:20:12 +08:00
{
if ( _isInitializing )
{
return ;
}
2026-03-15 17:08:07 +08:00
SyncCustomSeedPickerWithSavedThemeColor ( ) ;
UpdatePreview ( BuildPendingState ( usePickerSeed : false ) ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
public void SelectWallpaperSeed ( string value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
if ( ! IsWallpaperMode | | string . IsNullOrWhiteSpace ( value ) )
2026-03-13 22:20:12 +08:00
{
return ;
}
2026-03-15 17:08:07 +08:00
_selectedWallpaperSeed = value ;
PersistCurrentState ( restartRequired : true ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private void RefreshLocalizedText ( )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
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" ) ;
2026-03-20 00:41:14 +08:00
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." ) ;
2026-03-15 17:08:07 +08:00
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 ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private void ApplySavedState ( ThemeAppearanceSettingsState theme )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
IsNightMode = theme . IsNightMode ;
ThemeColor = theme . ThemeColor ? ? string . Empty ;
UseSystemChrome = theme . UseSystemChrome ;
2026-03-20 00:41:14 +08:00
GlobalCornerRadiusScale = GlobalAppearanceSettings . NormalizeCornerRadiusScale ( theme . GlobalCornerRadiusScale ) ;
2026-03-15 17:08:07 +08:00
_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 ] ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private void PersistCurrentState ( bool restartRequired )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
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 ) ;
2026-03-13 22:20:12 +08:00
2026-03-15 17:08:07 +08:00
if ( restartRequired )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
RestartRequested ? . Invoke ( AppearanceRestartMessage ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
}
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 ;
2026-03-13 22:20:12 +08:00
2026-03-15 17:08:07 +08:00
return new ThemeAppearanceSettingsState (
IsNightMode ,
themeColor ,
UseSystemChrome ,
2026-03-20 00:41:14 +08:00
GlobalAppearanceSettings . NormalizeCornerRadiusScale ( GlobalCornerRadiusScale ) ,
2026-03-15 17:08:07 +08:00
themeColorMode ,
ThemeAppearanceValues . NormalizeSystemMaterialMode ( SelectedSystemMaterialMode ? . Value ) ,
_selectedWallpaperSeed ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private void UpdatePreview ( ThemeAppearanceSettingsState pendingState )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
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
} ;
}
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private string ResolveMaterialModeLabel ( string value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
return ThemeAppearanceValues . NormalizeSystemMaterialMode ( value ) switch
{
ThemeAppearanceValues . MaterialMica = > SystemMaterialMicaText ,
ThemeAppearanceValues . MaterialAcrylic = > SystemMaterialAcrylicText ,
_ = > SystemMaterialNoneText
} ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 17:08:07 +08:00
private void SyncCustomSeedPickerWithSavedThemeColor ( )
2026-03-13 22:20:12 +08:00
{
2026-03-15 17:08:07 +08:00
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 )
] ;
2026-03-13 22:20:12 +08:00
}
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]
2026-03-20 00:41:14 +08:00
private string _componentsHeader = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _shortSideCellsLabel = string . Empty ;
[ObservableProperty]
private string _edgeInsetPercentLabel = string . Empty ;
[ObservableProperty]
private string _spacingPresetLabel = string . Empty ;
2026-03-20 00:41:14 +08:00
[ObservableProperty]
private double _globalCornerRadiusScale = GlobalAppearanceSettings . DefaultCornerRadiusScale ;
2026-03-20 18:05:42 +08:00
public double GlobalCornerRadiusMinimum = > GlobalAppearanceSettings . MinimumCornerRadiusScale ;
public double GlobalCornerRadiusMaximum = > GlobalAppearanceSettings . MaximumCornerRadiusScale ;
2026-03-20 00:41:14 +08:00
[ObservableProperty]
private string _componentRadiusHeader = string . Empty ;
[ObservableProperty]
private string _globalCornerRadiusLabel = string . Empty ;
[ObservableProperty]
private string _globalCornerRadiusDescription = string . Empty ;
2026-03-13 22:20:12 +08:00
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 ] ;
2026-03-20 00:41:14 +08:00
var theme = _settingsFacade . Theme . Get ( ) ;
GlobalCornerRadiusScale = GlobalAppearanceSettings . NormalizeCornerRadiusScale ( theme . GlobalCornerRadiusScale ) ;
2026-03-13 22:20:12 +08:00
}
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 ( ) ;
}
2026-03-20 00:41:14 +08:00
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 ( ) ;
}
2026-03-13 22:20:12 +08:00
private void SaveGrid ( )
{
_settingsFacade . Grid . Save ( new GridSettingsState (
Math . Clamp ( ShortSideCells , 6 , 96 ) ,
_settingsFacade . Grid . NormalizeSpacingPreset ( SelectedSpacingPreset . Value ) ,
Math . Clamp ( EdgeInsetPercent , 0 , 30 ) ) ) ;
}
2026-03-20 00:41:14 +08:00
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 ) ) ;
}
2026-03-13 22:20:12 +08:00
private IReadOnlyList < SelectionOption > CreateSpacingPresets ( )
{
return
[
2026-03-20 00:41:14 +08:00
new SelectionOption ( "Compact" , L ( "settings.components.spacing_compact" , "Compact" ) ) ,
new SelectionOption ( "Relaxed" , L ( "settings.components.spacing_relaxed" , "Relaxed" ) )
2026-03-13 22:20:12 +08:00
] ;
}
private void RefreshLocalizedText ( )
{
PageTitle = L ( "settings.components.title" , "Components" ) ;
2026-03-20 00:41:14 +08:00
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" ) ;
2026-03-20 18:05:42 +08:00
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." ) ;
2026-03-13 22:20:12 +08:00
}
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 ( )
{
2026-03-14 23:52:26 +08:00
if ( InstalledPlugins . Count > 0 )
2026-03-13 22:20:12 +08:00
{
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 ) ) ;
}
2026-03-14 23:52:26 +08:00
StatusMessage = string . Format (
CultureInfo . CurrentCulture ,
L (
"settings.plugins.refresh_success_installed_format" ,
"Loaded {0} installed plugins." ) ,
InstalledPlugins . Count ) ;
2026-03-13 22:20:12 +08:00
}
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" ) ;
2026-03-14 23:52:26 +08:00
PageDescription = L ( "settings.plugins.description" , "Manage installed plugins and review their runtime state." ) ;
2026-03-13 22:20:12 +08:00
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 ( ) ;
2026-03-15 04:35:34 +08:00
VersionText = _settingsFacade . ApplicationInfo . GetAppVersionText ( ) ;
CodenameText = _settingsFacade . ApplicationInfo . GetAppCodenameText ( ) ;
2026-03-13 22:20:12 +08:00
var backendInfo = _settingsFacade . ApplicationInfo . GetRenderBackendInfo ( ) ;
2026-03-15 04:35:34 +08:00
RenderBackendText = string . IsNullOrWhiteSpace ( backendInfo . ImplementationTypeName )
2026-03-13 22:20:12 +08:00
? backendInfo . ActualBackend
: $"{backendInfo.ActualBackend} ({backendInfo.ImplementationTypeName})" ;
}
[ObservableProperty]
private string _versionText = "-" ;
2026-03-15 04:35:34 +08:00
[ObservableProperty]
private string _codenameText = "-" ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _renderBackendText = "-" ;
2026-03-15 04:35:34 +08:00
[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 ( ) ;
}
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _selectedUpdateChannelValue = UpdateSettingsValues . ChannelStable ;
[ObservableProperty]
private string _selectedUpdateSourceValue = UpdateSettingsValues . DownloadSourceGitHub ;
[ObservableProperty]
private string _selectedUpdateModeValue = UpdateSettingsValues . ModeDownloadThenConfirm ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _currentVersionText = "-" ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _updateStatus = string . Empty ;
[ObservableProperty]
private bool _isCheckingForUpdates ;
2026-03-15 04:35:34 +08:00
[ObservableProperty]
private bool _isDownloading ;
[ObservableProperty]
private double _downloadProgressValue ;
[ObservableProperty]
private bool _isDownloadProgressVisible ;
[ObservableProperty]
private string _downloadProgressText = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _pageTitle = string . Empty ;
[ObservableProperty]
private string _pageDescription = string . Empty ;
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _statusCardTitle = string . Empty ;
[ObservableProperty]
private string _statusCardDescription = string . Empty ;
[ObservableProperty]
private string _preferencesHeader = string . Empty ;
[ObservableProperty]
private string _preferencesDescription = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _updateChannelLabel = string . Empty ;
2026-03-15 04:35:34 +08:00
[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 ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
private string _checkForUpdatesButtonText = string . Empty ;
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _downloadButtonText = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _installNowButtonText = string . Empty ;
2026-03-13 22:20:12 +08:00
2026-03-25 11:27:30 +08:00
[ObservableProperty]
private string _redownloadButtonText = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 04:35:34 +08:00
private string _latestVersionText = string . Empty ;
2026-03-13 22:20:12 +08:00
[ObservableProperty]
2026-03-15 04:35:34 +08:00
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 ;
2026-03-25 11:27:30 +08:00
[ObservableProperty]
private string _forceCheckUpdateLabel = string . Empty ;
[ObservableProperty]
private string _forceCheckUpdateDescription = string . Empty ;
2026-03-15 04:35:34 +08:00
[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 ;
2026-03-25 11:27:30 +08:00
public bool IsRedownloadButtonVisible = > HasPendingInstaller & & ! IsDownloading ;
2026-03-15 04:35:34 +08:00
public string DownloadThreadsValueText = >
UpdateSettingsValues . NormalizeDownloadThreads ( ( int ) Math . Round ( DownloadThreadsSliderValue ) ) . ToString ( CultureInfo . CurrentCulture ) ;
private bool IsBusy = > IsCheckingForUpdates | | IsDownloading ;
2026-03-13 22:20:12 +08:00
2026-03-15 04:35:34 +08:00
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 )
2026-03-13 22:20:12 +08:00
{
if ( _isInitializing )
{
return ;
}
SaveUpdateSettings ( ) ;
2026-03-15 04:35:34 +08:00
SelectedUpdateSourceDescription = BuildUpdateSourceDescription ( value ) ;
UpdateStatus = L ( "settings.update.status_preferences_saved" , "Update preferences saved." ) ;
SyncSelectedOptions ( ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 04:35:34 +08:00
partial void OnSelectedUpdateModeValueChanged ( string value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 04:35:34 +08:00
if ( _isInitializing )
2026-03-13 22:20:12 +08:00
{
return ;
}
SaveUpdateSettings ( ) ;
2026-03-15 04:35:34 +08:00
SelectedUpdateModeDescription = BuildUpdateModeDescription ( value ) ;
UpdateStatus = HasPendingInstaller
? BuildPendingReadyStatus ( )
: L ( "settings.update.status_preferences_saved" , "Update preferences saved." ) ;
SyncSelectedOptions ( ) ;
RefreshActionState ( ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 04:35:34 +08:00
partial void OnDownloadThreadsSliderValueChanged ( double value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 04:35:34 +08:00
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 ( ) ;
2026-03-13 22:20:12 +08:00
UpdateStatus = L ( "settings.update.status_preferences_saved" , "Update preferences saved." ) ;
2026-03-15 04:35:34 +08:00
SyncSelectedOptions ( ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-15 04:35:34 +08:00
partial void OnDownloadThreadsTextChanged ( string value )
2026-03-13 22:20:12 +08:00
{
2026-03-15 04:35:34 +08:00
if ( _isInitializing )
{
return ;
}
if ( ! TryParseDownloadThreads ( value , out var parsed ) )
2026-03-13 22:20:12 +08:00
{
return ;
}
2026-03-15 04:35:34 +08:00
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 ( )
2026-03-25 11:27:30 +08:00
{
await CheckForUpdatesCoreAsync ( isForce : false ) ;
}
private bool CanForceCheckUpdate ( ) = > ! IsBusy ;
[RelayCommand(CanExecute = nameof(CanForceCheckUpdate))]
private async Task ForceCheckUpdateAsync ( )
{
await CheckForUpdatesCoreAsync ( isForce : true ) ;
}
private async Task CheckForUpdatesCoreAsync ( bool isForce )
2026-03-15 04:35:34 +08:00
{
2026-03-13 22:20:12 +08:00
try
{
IsCheckingForUpdates = true ;
2026-03-15 04:35:34 +08:00
IsDownloadProgressVisible = false ;
DownloadProgressValue = 0 ;
DownloadProgressText = L ( "settings.update.download_progress_idle" , "Download progress: -" ) ;
2026-03-25 11:27:30 +08:00
UpdateStatus = isForce
? L ( "settings.update.status_force_checking" , "Force checking GitHub releases..." )
: L ( "settings.update.status_checking" , "Checking GitHub releases..." ) ;
2026-03-15 04:35:34 +08:00
2026-03-25 11:27:30 +08:00
var result = await _updateWorkflowService . CheckForUpdatesAsync ( _currentVersion , isForce ) ;
2026-03-15 04:35:34 +08:00
_lastCheckResult = result . Success ? result : null ;
RefreshLastCheckedFromSettings ( ) ;
2026-03-13 22:20:12 +08:00
if ( ! result . Success )
{
UpdateStatus = string . IsNullOrWhiteSpace ( result . ErrorMessage )
? L ( "settings.update.status_check_failed" , "Failed to check for updates." )
2026-03-15 04:35:34 +08:00
: string . Format (
CultureInfo . CurrentCulture ,
L ( "settings.update.status_check_failed_format" , "Update check failed: {0}" ) ,
result . ErrorMessage ) ;
2026-03-13 22:20:12 +08:00
return ;
}
2026-03-15 04:35:34 +08:00
ApplyCheckResultDisplay ( result ) ;
2026-03-25 11:27:30 +08:00
if ( ! result . IsUpdateAvailable & & ! isForce )
2026-03-15 04:35:34 +08:00
{
return ;
}
if ( result . PreferredAsset is null )
{
2026-03-25 11:27:30 +08:00
UpdateStatus = isForce
? L ( "settings.update.status_force_no_asset" , "Release found but no compatible installer available." )
: L ( "settings.update.status_asset_missing" , "A new release is available, but no compatible installer was found." ) ;
2026-03-15 04:35:34 +08:00
return ;
}
if ( ! string . Equals ( SelectedUpdateModeValue , UpdateSettingsValues . ModeManual , StringComparison . OrdinalIgnoreCase ) )
{
await DownloadLatestReleaseCoreAsync ( result , invokedFromCheck : true ) ;
return ;
}
UpdateStatus = string . Format (
CultureInfo . CurrentCulture ,
2026-03-25 11:27:30 +08:00
isForce
? L ( "settings.update.status_force_available_format" , "Release {0} is available. Click Download & Install." )
: L ( "settings.update.status_available_format" , "New version {0} is available. Click Download & Install." ) ,
2026-03-15 04:35:34 +08:00
result . LatestVersionText ) ;
2026-03-13 22:20:12 +08:00
}
finally
{
IsCheckingForUpdates = false ;
}
}
2026-03-15 04:35:34 +08:00
private bool CanDownloadLatestRelease ( ) = > ! IsBusy & & IsDownloadButtonVisible ;
[RelayCommand(CanExecute = nameof(CanDownloadLatestRelease))]
private async Task DownloadLatestReleaseAsync ( )
2026-03-13 22:20:12 +08:00
{
2026-03-15 04:35:34 +08:00
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." ) ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-25 11:27:30 +08:00
private bool CanRedownloadUpdate ( ) = > ! IsBusy & & HasPendingInstaller & & _lastCheckResult is not null ;
[RelayCommand(CanExecute = nameof(CanRedownloadUpdate))]
private async Task RedownloadUpdateAsync ( )
{
if ( _lastCheckResult is null | | ! _lastCheckResult . Success | | ! _lastCheckResult . IsUpdateAvailable | | _lastCheckResult . PreferredAsset is null )
{
UpdateStatus = L ( "settings.update.status_redownload_no_check" , "Please check for updates first before redownloading." ) ;
return ;
}
try
{
IsDownloading = true ;
IsDownloadProgressVisible = true ;
DownloadProgressValue = 0 ;
DownloadProgressText = L ( "settings.update.download_progress_idle" , "Download progress: -" ) ;
UpdateStatus = L ( "settings.update.status_redownloading" , "Redownloading 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 . RedownloadReleaseAsync ( _lastCheckResult , progress ) ;
if ( ! downloadResult . Success )
{
UpdateStatus = string . Format (
CultureInfo . CurrentCulture ,
L ( "settings.update.status_redownload_failed_format" , "Redownload failed: {0}" ) ,
downloadResult . ErrorMessage ? ? L ( "settings.update.status_check_failed" , "Failed to check for updates." ) ) ;
return ;
}
ApplyPendingState ( _settingsFacade . Update . Get ( ) ) ;
UpdateStatus = downloadResult . HashVerified
? BuildPendingReadyStatus ( )
: string . Format (
CultureInfo . CurrentCulture ,
L ( "settings.update.status_downloaded_no_hash_format" , "Update downloaded. Hash: {0}" ) ,
downloadResult . ActualHash ? ? "N/A" ) ;
}
finally
{
IsDownloading = false ;
IsDownloadProgressVisible = false ;
}
}
2026-03-13 22:20:12 +08:00
private void RefreshLocalizedText ( )
{
2026-03-15 04:35:34 +08:00
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." ) ;
2026-03-13 22:20:12 +08:00
UpdateChannelLabel = L ( "settings.update.channel_label" , "Update Channel" ) ;
2026-03-15 04:35:34 +08:00
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." ) ;
2026-03-25 11:27:30 +08:00
ForceCheckUpdateLabel = L ( "settings.update.force_check_label" , "Force Check Update" ) ;
ForceCheckUpdateDescription = L ( "settings.update.force_check_desc" , "Force check for updates from GitHub, ignoring version comparison." ) ;
2026-03-13 22:20:12 +08:00
CheckForUpdatesButtonText = L ( "settings.update.check_button" , "Check for Updates" ) ;
2026-03-15 04:35:34 +08:00
DownloadButtonText = L ( "settings.update.download_install_button" , "Download & Install" ) ;
InstallNowButtonText = L ( "settings.update.install_now_button" , "Install Now" ) ;
2026-03-25 11:27:30 +08:00
RedownloadButtonText = L ( "settings.update.redownload_button" , "Redownload" ) ;
2026-03-15 04:35:34 +08:00
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 ) ) ;
2026-03-25 11:27:30 +08:00
OnPropertyChanged ( nameof ( IsRedownloadButtonVisible ) ) ;
2026-03-15 04:35:34 +08:00
OnPropertyChanged ( nameof ( DownloadThreadsValueText ) ) ;
2026-03-25 11:27:30 +08:00
RedownloadUpdateCommand . NotifyCanExecuteChanged ( ) ;
2026-03-15 04:35:34 +08:00
}
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 ) ;
2026-03-13 22:20:12 +08:00
}
2026-03-30 02:40:10 +08:00
private string L ( string key , string fallback )
= > _localizationService . GetString ( _languageCode , key , fallback ) ;
}
public sealed partial class StudySettingsPageViewModel : ViewModelBase
{
private readonly ISettingsFacadeService _settingsFacade ;
private readonly LocalizationService _localizationService = new ( ) ;
private readonly string _languageCode ;
private bool _isInitializing ;
2026-04-02 11:54:58 +08:00
private readonly IStudyAnalyticsService _studyAnalyticsService ;
2026-03-30 02:40:10 +08:00
2026-04-02 11:54:58 +08:00
// 防抖计时器
private System . Timers . Timer ? _noiseSettingsDebounceTimer ;
private System . Timers . Timer ? _timerSettingsDebounceTimer ;
private System . Timers . Timer ? _alertSettingsDebounceTimer ;
private System . Timers . Timer ? _displaySettingsDebounceTimer ;
private bool _hasPendingNoiseSave ;
private bool _hasPendingTimerSave ;
private bool _hasPendingAlertSave ;
private bool _hasPendingDisplaySave ;
public StudySettingsPageViewModel ( ISettingsFacadeService settingsFacade , IStudyAnalyticsService ? studyAnalyticsService = null )
2026-03-30 02:40:10 +08:00
{
_settingsFacade = settingsFacade ? ? throw new ArgumentNullException ( nameof ( settingsFacade ) ) ;
2026-04-02 11:54:58 +08:00
_studyAnalyticsService = studyAnalyticsService ? ? StudyAnalyticsServiceFactory . CreateDefault ( ) ;
2026-03-30 02:40:10 +08:00
_languageCode = _localizationService . NormalizeLanguageCode ( _settingsFacade . Region . Get ( ) . LanguageCode ) ;
2026-04-02 11:54:58 +08:00
// 初始化防抖计时器
InitializeDebounceTimers ( ) ;
2026-03-30 02:40:10 +08:00
RefreshLocalizedText ( ) ;
_isInitializing = true ;
LoadSettings ( ) ;
_isInitializing = false ;
}
2026-04-02 11:54:58 +08:00
private void InitializeDebounceTimers ( )
{
_noiseSettingsDebounceTimer = new System . Timers . Timer ( 500 ) { AutoReset = false } ;
_noiseSettingsDebounceTimer . Elapsed + = async ( s , e ) = > await SaveNoiseSettingsDebounced ( ) ;
_timerSettingsDebounceTimer = new System . Timers . Timer ( 500 ) { AutoReset = false } ;
_timerSettingsDebounceTimer . Elapsed + = async ( s , e ) = > await SaveTimerSettingsDebounced ( ) ;
_alertSettingsDebounceTimer = new System . Timers . Timer ( 500 ) { AutoReset = false } ;
_alertSettingsDebounceTimer . Elapsed + = async ( s , e ) = > await SaveAlertSettingsDebounced ( ) ;
_displaySettingsDebounceTimer = new System . Timers . Timer ( 500 ) { AutoReset = false } ;
_displaySettingsDebounceTimer . Elapsed + = async ( s , e ) = > await SaveDisplaySettingsDebounced ( ) ;
}
2026-03-31 01:16:53 +08:00
#region Properties - Master Switch
[ObservableProperty]
private string _masterSwitchHeader = string . Empty ;
[ObservableProperty]
private string _masterSwitchDescription = string . Empty ;
[ObservableProperty]
private bool _studyEnabled = true ;
partial void OnStudyEnabledChanged ( bool value )
{
if ( ! _isInitializing )
{
SaveMasterSwitch ( ) ;
}
}
2026-04-02 11:54:58 +08:00
private void SaveMasterSwitch ( )
{
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
appSnapshot . StudyEnabled = StudyEnabled ;
_settingsFacade . Settings . SaveSnapshot ( SettingsScope . App , appSnapshot ,
changedKeys : [ nameof ( AppSettingsSnapshot . StudyEnabled ) ] ) ;
}
catch ( Exception )
{
// 静默处理错误,避免影响用户体验
}
}
2026-03-31 01:16:53 +08:00
#endregion
2026-03-30 02:40:10 +08:00
#region Properties - Noise Monitoring
[ObservableProperty]
private string _noiseMonitoringHeader = string . Empty ;
[ObservableProperty]
private string _noiseMonitoringDescription = string . Empty ;
[ObservableProperty]
private string _samplingRateLabel = string . Empty ;
[ObservableProperty]
private string _samplingRateDescription = string . Empty ;
[ObservableProperty]
private int _samplingRateMs = 50 ;
[ObservableProperty]
private string _samplingRateValueText = string . Empty ;
[ObservableProperty]
private string _noiseSensitivityLabel = string . Empty ;
[ObservableProperty]
private string _noiseSensitivityDescription = string . Empty ;
[ObservableProperty]
private double _noiseSensitivityDbfs = - 50 ;
[ObservableProperty]
private string _noiseSensitivityValueText = string . Empty ;
[ObservableProperty]
private string _currentThresholdText = string . Empty ;
partial void OnNoiseSensitivityDbfsChanged ( double value )
{
2026-04-02 11:54:58 +08:00
// 输入验证:限制在合理范围内
if ( value < - 70 | | value > - 35 )
{
NoiseSensitivityDbfs = Math . Clamp ( value , - 70 , - 35 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateSensitivityText ( ) ;
UpdateThresholdText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceNoiseSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnSamplingRateMsChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证:限制在合理范围内
if ( value < 20 | | value > 200 )
{
SamplingRateMs = Math . Clamp ( value , 20 , 200 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateSamplingRateText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceNoiseSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
private void UpdateSamplingRateText ( )
{
SamplingRateValueText = $"{SamplingRateMs}ms" ;
}
private void UpdateSensitivityText ( )
{
NoiseSensitivityValueText = $"{NoiseSensitivityDbfs:F0} dBFS" ;
}
2026-04-02 11:54:58 +08:00
private void DebounceNoiseSettingsSave ( )
{
_hasPendingNoiseSave = true ;
_noiseSettingsDebounceTimer ? . Stop ( ) ;
_noiseSettingsDebounceTimer ? . Start ( ) ;
}
private async Task SaveNoiseSettingsDebounced ( )
{
if ( ! _hasPendingNoiseSave ) return ;
_hasPendingNoiseSave = false ;
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
appSnapshot . StudyFrameMs = SamplingRateMs ;
appSnapshot . StudyScoreThresholdDbfs = NoiseSensitivityDbfs ;
_settingsFacade . Settings . SaveSnapshot ( SettingsScope . App , appSnapshot ,
changedKeys : [ nameof ( AppSettingsSnapshot . StudyFrameMs ) , nameof ( AppSettingsSnapshot . StudyScoreThresholdDbfs ) ] ) ;
UpdateThresholdText ( ) ;
UpdateStudyAnalyticsConfig ( ) ;
}
catch ( Exception )
{
// 静默处理错误
}
}
2026-03-30 02:40:10 +08:00
#endregion
#region Properties - Focus Timer
[ObservableProperty]
private string _focusTimerHeader = string . Empty ;
[ObservableProperty]
private string _focusTimerDescription = string . Empty ;
[ObservableProperty]
private string _focusDurationLabel = string . Empty ;
[ObservableProperty]
private string _focusDurationDescription = string . Empty ;
[ObservableProperty]
private int _focusDurationMinutes = 25 ;
[ObservableProperty]
private string _focusDurationValueText = string . Empty ;
[ObservableProperty]
private string _breakDurationLabel = string . Empty ;
[ObservableProperty]
private string _breakDurationDescription = string . Empty ;
[ObservableProperty]
private int _breakDurationMinutes = 5 ;
[ObservableProperty]
private string _breakDurationValueText = string . Empty ;
[ObservableProperty]
private string _longBreakDurationLabel = string . Empty ;
[ObservableProperty]
private string _longBreakDurationDescription = string . Empty ;
[ObservableProperty]
private int _longBreakDurationMinutes = 15 ;
[ObservableProperty]
private string _longBreakDurationValueText = string . Empty ;
[ObservableProperty]
private string _sessionsBeforeLongBreakLabel = string . Empty ;
[ObservableProperty]
private string _sessionsBeforeLongBreakDescription = string . Empty ;
[ObservableProperty]
private int _sessionsBeforeLongBreak = 4 ;
[ObservableProperty]
private string _sessionsBeforeLongBreakValueText = string . Empty ;
[ObservableProperty]
private string _autoStartBreakLabel = string . Empty ;
[ObservableProperty]
private string _autoStartBreakDescription = string . Empty ;
[ObservableProperty]
private bool _autoStartBreak ;
[ObservableProperty]
private string _autoStartFocusLabel = string . Empty ;
[ObservableProperty]
private string _autoStartFocusDescription = string . Empty ;
[ObservableProperty]
private bool _autoStartFocus ;
partial void OnFocusDurationMinutesChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 5 | | value > 90 )
{
FocusDurationMinutes = Math . Clamp ( value , 5 , 90 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateFocusDurationText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnBreakDurationMinutesChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 1 | | value > 30 )
{
BreakDurationMinutes = Math . Clamp ( value , 1 , 30 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateBreakDurationText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnLongBreakDurationMinutesChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 5 | | value > 60 )
{
LongBreakDurationMinutes = Math . Clamp ( value , 5 , 60 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateLongBreakDurationText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnSessionsBeforeLongBreakChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 2 | | value > 8 )
{
SessionsBeforeLongBreak = Math . Clamp ( value , 2 , 8 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateSessionsBeforeLongBreakText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnAutoStartBreakChanged ( bool value )
{
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnAutoStartFocusChanged ( bool value )
{
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceTimerSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
private void UpdateFocusDurationText ( )
{
2026-04-02 11:54:58 +08:00
var unit = L ( "common.unit.minutes" , "分钟" ) ;
FocusDurationValueText = $"{FocusDurationMinutes} {unit}" ;
2026-03-30 02:40:10 +08:00
}
private void UpdateBreakDurationText ( )
{
2026-04-02 11:54:58 +08:00
var unit = L ( "common.unit.minutes" , "分钟" ) ;
BreakDurationValueText = $"{BreakDurationMinutes} {unit}" ;
2026-03-30 02:40:10 +08:00
}
private void UpdateLongBreakDurationText ( )
{
2026-04-02 11:54:58 +08:00
var unit = L ( "common.unit.minutes" , "分钟" ) ;
LongBreakDurationValueText = $"{LongBreakDurationMinutes} {unit}" ;
2026-03-30 02:40:10 +08:00
}
private void UpdateSessionsBeforeLongBreakText ( )
{
2026-04-02 11:54:58 +08:00
var unit = L ( "common.unit.times" , "次" ) ;
SessionsBeforeLongBreakValueText = $"{SessionsBeforeLongBreak} {unit}" ;
}
private void DebounceTimerSettingsSave ( )
{
_hasPendingTimerSave = true ;
_timerSettingsDebounceTimer ? . Stop ( ) ;
_timerSettingsDebounceTimer ? . Start ( ) ;
}
private async Task SaveTimerSettingsDebounced ( )
{
if ( ! _hasPendingTimerSave ) return ;
_hasPendingTimerSave = false ;
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
appSnapshot . StudyFocusDurationMinutes = FocusDurationMinutes ;
appSnapshot . StudyBreakDurationMinutes = BreakDurationMinutes ;
appSnapshot . StudyLongBreakDurationMinutes = LongBreakDurationMinutes ;
appSnapshot . StudySessionsBeforeLongBreak = SessionsBeforeLongBreak ;
appSnapshot . StudyAutoStartBreak = AutoStartBreak ;
appSnapshot . StudyAutoStartFocus = AutoStartFocus ;
_settingsFacade . Settings . SaveSnapshot ( SettingsScope . App , appSnapshot ,
changedKeys : [
nameof ( AppSettingsSnapshot . StudyFocusDurationMinutes ) ,
nameof ( AppSettingsSnapshot . StudyBreakDurationMinutes ) ,
nameof ( AppSettingsSnapshot . StudyLongBreakDurationMinutes ) ,
nameof ( AppSettingsSnapshot . StudySessionsBeforeLongBreak ) ,
nameof ( AppSettingsSnapshot . StudyAutoStartBreak ) ,
nameof ( AppSettingsSnapshot . StudyAutoStartFocus )
] ) ;
}
catch ( Exception )
{
// 静默处理错误
}
2026-03-30 02:40:10 +08:00
}
#endregion
#region Properties - Alert
[ObservableProperty]
private string _alertHeader = string . Empty ;
[ObservableProperty]
private string _alertDescription = string . Empty ;
[ObservableProperty]
private string _noiseAlertEnabledLabel = string . Empty ;
[ObservableProperty]
private string _noiseAlertEnabledDescription = string . Empty ;
[ObservableProperty]
private bool _noiseAlertEnabled ;
[ObservableProperty]
private string _maxInterruptsPerMinuteLabel = string . Empty ;
[ObservableProperty]
private string _maxInterruptsPerMinuteDescription = string . Empty ;
[ObservableProperty]
private int _maxInterruptsPerMinute = 6 ;
partial void OnNoiseAlertEnabledChanged ( bool value )
{
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceAlertSettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnMaxInterruptsPerMinuteChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 3 | | value > 20 )
{
MaxInterruptsPerMinute = Math . Clamp ( value , 3 , 20 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceAlertSettingsSave ( ) ;
}
}
private void DebounceAlertSettingsSave ( )
{
_hasPendingAlertSave = true ;
_alertSettingsDebounceTimer ? . Stop ( ) ;
_alertSettingsDebounceTimer ? . Start ( ) ;
}
private async Task SaveAlertSettingsDebounced ( )
{
if ( ! _hasPendingAlertSave ) return ;
_hasPendingAlertSave = false ;
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
appSnapshot . StudyNoiseAlertEnabled = NoiseAlertEnabled ;
appSnapshot . StudyMaxInterruptsPerMinute = MaxInterruptsPerMinute ;
_settingsFacade . Settings . SaveSnapshot ( SettingsScope . App , appSnapshot ,
changedKeys : [ nameof ( AppSettingsSnapshot . StudyNoiseAlertEnabled ) , nameof ( AppSettingsSnapshot . StudyMaxInterruptsPerMinute ) ] ) ;
UpdateStudyAnalyticsConfig ( ) ;
}
catch ( Exception )
{
// 静默处理错误
2026-03-30 02:40:10 +08:00
}
}
#endregion
#region Properties - Display
[ObservableProperty]
private string _displayHeader = string . Empty ;
[ObservableProperty]
private string _displayDescription = string . Empty ;
[ObservableProperty]
private string _showRealtimeDbLabel = string . Empty ;
[ObservableProperty]
private string _showRealtimeDbDescription = string . Empty ;
[ObservableProperty]
private bool _showRealtimeDb = true ;
[ObservableProperty]
private string _baselineDbLabel = string . Empty ;
[ObservableProperty]
private string _baselineDbDescription = string . Empty ;
[ObservableProperty]
private double _baselineDb = 45 ;
[ObservableProperty]
private string _baselineDbValueText = string . Empty ;
[ObservableProperty]
private string _avgWindowSecLabel = string . Empty ;
[ObservableProperty]
private string _avgWindowSecDescription = string . Empty ;
[ObservableProperty]
private int _avgWindowSec = 1 ;
[ObservableProperty]
private string _avgWindowSecValueText = string . Empty ;
partial void OnShowRealtimeDbChanged ( bool value )
{
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceDisplaySettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnBaselineDbChanged ( double value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 20 | | value > 90 )
{
BaselineDb = Math . Clamp ( value , 20 , 90 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateBaselineDbText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceDisplaySettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
partial void OnAvgWindowSecChanged ( int value )
{
2026-04-02 11:54:58 +08:00
// 输入验证
if ( value < 1 | | value > 8 )
{
AvgWindowSec = Math . Clamp ( value , 1 , 8 ) ;
return ;
}
2026-03-30 02:40:10 +08:00
UpdateAvgWindowSecText ( ) ;
if ( ! _isInitializing )
{
2026-04-02 11:54:58 +08:00
DebounceDisplaySettingsSave ( ) ;
2026-03-30 02:40:10 +08:00
}
}
#endregion
[ObservableProperty]
private string _footerHint = string . Empty ;
private void UpdateBaselineDbText ( )
{
BaselineDbValueText = $"{BaselineDb:F0} dB" ;
}
private void UpdateAvgWindowSecText ( )
{
2026-04-02 11:54:58 +08:00
var unit = L ( "common.unit.seconds" , "秒" ) ;
AvgWindowSecValueText = $"{AvgWindowSec} {unit}" ;
2026-03-30 02:40:10 +08:00
}
2026-04-02 11:54:58 +08:00
private void DebounceDisplaySettingsSave ( )
2026-03-31 01:16:53 +08:00
{
2026-04-02 11:54:58 +08:00
_hasPendingDisplaySave = true ;
_displaySettingsDebounceTimer ? . Stop ( ) ;
_displaySettingsDebounceTimer ? . Start ( ) ;
2026-03-31 01:16:53 +08:00
}
2026-04-02 11:54:58 +08:00
private async Task SaveDisplaySettingsDebounced ( )
2026-03-30 02:40:10 +08:00
{
2026-04-02 11:54:58 +08:00
if ( ! _hasPendingDisplaySave ) return ;
_hasPendingDisplaySave = false ;
2026-03-30 02:40:10 +08:00
2026-04-02 11:54:58 +08:00
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
appSnapshot . StudyShowRealtimeDb = ShowRealtimeDb ;
appSnapshot . StudyBaselineDb = BaselineDb ;
appSnapshot . StudyAvgWindowSec = AvgWindowSec ;
_settingsFacade . Settings . SaveSnapshot ( SettingsScope . App , appSnapshot ,
changedKeys : [ nameof ( AppSettingsSnapshot . StudyShowRealtimeDb ) , nameof ( AppSettingsSnapshot . StudyBaselineDb ) , nameof ( AppSettingsSnapshot . StudyAvgWindowSec ) ] ) ;
UpdateStudyAnalyticsConfig ( ) ;
}
catch ( Exception )
{
// 静默处理错误
}
2026-03-30 02:40:10 +08:00
}
2026-04-02 11:54:58 +08:00
private void LoadSettings ( )
2026-03-30 02:40:10 +08:00
{
2026-04-02 11:54:58 +08:00
try
{
var appSnapshot = _settingsFacade . Settings . LoadSnapshot < AppSettingsSnapshot > ( SettingsScope . App ) ;
// Master switch - 确保正确加载保存的值
StudyEnabled = appSnapshot . StudyEnabled ;
// Noise settings
SamplingRateMs = appSnapshot . StudyFrameMs is > 0 ? appSnapshot . StudyFrameMs . Value : 50 ;
NoiseSensitivityDbfs = appSnapshot . StudyScoreThresholdDbfs ? ? - 50 ;
// Timer settings
FocusDurationMinutes = appSnapshot . StudyFocusDurationMinutes is > 0 ? appSnapshot . StudyFocusDurationMinutes . Value : 25 ;
BreakDurationMinutes = appSnapshot . StudyBreakDurationMinutes is > 0 ? appSnapshot . StudyBreakDurationMinutes . Value : 5 ;
LongBreakDurationMinutes = appSnapshot . StudyLongBreakDurationMinutes is > 0 ? appSnapshot . StudyLongBreakDurationMinutes . Value : 15 ;
SessionsBeforeLongBreak = appSnapshot . StudySessionsBeforeLongBreak is > 0 ? appSnapshot . StudySessionsBeforeLongBreak . Value : 4 ;
AutoStartBreak = appSnapshot . StudyAutoStartBreak ? ? false ;
AutoStartFocus = appSnapshot . StudyAutoStartFocus ? ? false ;
// Alert settings
NoiseAlertEnabled = appSnapshot . StudyNoiseAlertEnabled ? ? false ;
MaxInterruptsPerMinute = appSnapshot . StudyMaxInterruptsPerMinute is > 0 ? appSnapshot . StudyMaxInterruptsPerMinute . Value : 6 ;
// Display settings
ShowRealtimeDb = appSnapshot . StudyShowRealtimeDb ? ? true ;
BaselineDb = appSnapshot . StudyBaselineDb ? ? 45 ;
AvgWindowSec = appSnapshot . StudyAvgWindowSec ? ? 1 ;
UpdateSamplingRateText ( ) ;
UpdateSensitivityText ( ) ;
UpdateThresholdText ( ) ;
UpdateFocusDurationText ( ) ;
UpdateBreakDurationText ( ) ;
UpdateLongBreakDurationText ( ) ;
UpdateSessionsBeforeLongBreakText ( ) ;
UpdateBaselineDbText ( ) ;
UpdateAvgWindowSecText ( ) ;
}
catch ( Exception )
{
// 加载失败时使用默认值
StudyEnabled = true ;
SamplingRateMs = 50 ;
NoiseSensitivityDbfs = - 50 ;
FocusDurationMinutes = 25 ;
BreakDurationMinutes = 5 ;
LongBreakDurationMinutes = 15 ;
SessionsBeforeLongBreak = 4 ;
AutoStartBreak = false ;
AutoStartFocus = false ;
NoiseAlertEnabled = false ;
MaxInterruptsPerMinute = 6 ;
ShowRealtimeDb = true ;
BaselineDb = 45 ;
AvgWindowSec = 1 ;
}
2026-03-30 02:40:10 +08:00
}
private void UpdateStudyAnalyticsConfig ( )
{
var currentConfig = _studyAnalyticsService . GetConfig ( ) ;
var newConfig = currentConfig with
{
FrameMs = SamplingRateMs ,
ScoreThresholdDbfs = NoiseSensitivityDbfs ,
BaselineDb = BaselineDb ,
AvgWindowSec = AvgWindowSec ,
ShowRelativeDb = ShowRealtimeDb ,
MaxSegmentsPerMin = MaxInterruptsPerMinute ,
AlertSoundEnabled = NoiseAlertEnabled
} ;
_studyAnalyticsService . UpdateConfig ( newConfig ) ;
}
private void UpdateThresholdText ( )
{
CurrentThresholdText = string . Format (
CultureInfo . CurrentCulture ,
L ( "settings.study.current_threshold_format" , "当前评分阈值: {0} dBFS" ) ,
NoiseSensitivityDbfs ) ;
}
private void RefreshLocalizedText ( )
{
2026-03-31 01:16:53 +08:00
MasterSwitchHeader = L ( "settings.study.master_switch_header" , "自习功能" ) ;
MasterSwitchDescription = L ( "settings.study.master_switch_desc" , "启用自习环境监测和专注计时功能。关闭后,相关组件将不会采集任何数据。" ) ;
2026-03-30 02:40:10 +08:00
NoiseMonitoringHeader = L ( "settings.study.noise_header" , "噪音监测" ) ;
NoiseMonitoringDescription = L ( "settings.study.noise_description" , "配置麦克风采集频率和噪音评分敏感度。" ) ;
SamplingRateLabel = L ( "settings.study.sampling_rate_label" , "采集频率" ) ;
SamplingRateDescription = L ( "settings.study.sampling_rate_desc" , "麦克风采集音频的时间间隔。更高的频率会更准确地捕捉噪音变化,但会增加电量消耗。" ) ;
NoiseSensitivityLabel = L ( "settings.study.sensitivity_label" , "噪音敏感度" ) ;
NoiseSensitivityDescription = L ( "settings.study.sensitivity_desc" , "评分阈值决定了什么级别的噪音会被认为是干扰。阈值越严格,越容易检测到轻微噪音。" ) ;
FocusTimerHeader = L ( "settings.study.timer_header" , "专注计时" ) ;
FocusTimerDescription = L ( "settings.study.timer_description" , "配置专注时段和休息时段的时长。" ) ;
FocusDurationLabel = L ( "settings.study.focus_duration_label" , "专注时长" ) ;
FocusDurationDescription = L ( "settings.study.focus_duration_desc" , "单次专注时段的持续时间(分钟)。" ) ;
BreakDurationLabel = L ( "settings.study.break_duration_label" , "休息时长" ) ;
BreakDurationDescription = L ( "settings.study.break_duration_desc" , "短休息时段的持续时间(分钟)。" ) ;
LongBreakDurationLabel = L ( "settings.study.long_break_duration_label" , "长休息时长" ) ;
LongBreakDurationDescription = L ( "settings.study.long_break_duration_desc" , "长休息时段的持续时间(分钟)。" ) ;
SessionsBeforeLongBreakLabel = L ( "settings.study.sessions_before_long_break_label" , "长休息间隔" ) ;
SessionsBeforeLongBreakDescription = L ( "settings.study.sessions_before_long_break_desc" , "经过几个专注时段后触发长休息。" ) ;
AutoStartBreakLabel = L ( "settings.study.auto_start_break_label" , "自动开始休息" ) ;
AutoStartBreakDescription = L ( "settings.study.auto_start_break_desc" , "专注时段结束后自动开始休息计时。" ) ;
AutoStartFocusLabel = L ( "settings.study.auto_start_focus_label" , "自动开始专注" ) ;
AutoStartFocusDescription = L ( "settings.study.auto_start_focus_desc" , "休息时段结束后自动开始专注计时。" ) ;
AlertHeader = L ( "settings.study.alert_header" , "提醒设置" ) ;
AlertDescription = L ( "settings.study.alert_description" , "配置噪音干扰提醒。" ) ;
NoiseAlertEnabledLabel = L ( "settings.study.noise_alert_enabled_label" , "启用噪音提醒" ) ;
NoiseAlertEnabledDescription = L ( "settings.study.noise_alert_enabled_desc" , "当检测到超过容忍阈值的噪音干扰时显示提醒。" ) ;
MaxInterruptsPerMinuteLabel = L ( "settings.study.max_interrupts_label" , "最大容忍打断次数" ) ;
MaxInterruptsPerMinuteDescription = L ( "settings.study.max_interrupts_desc" , "每分钟最多允许多少次噪音干扰事件,超过此值将触发提醒。" ) ;
DisplayHeader = L ( "settings.study.display_header" , "显示设置" ) ;
DisplayDescription = L ( "settings.study.display_description" , "配置噪音数据的显示方式。" ) ;
ShowRealtimeDbLabel = L ( "settings.study.show_realtime_db_label" , "显示实时分贝" ) ;
ShowRealtimeDbDescription = L ( "settings.study.show_realtime_db_desc" , "在组件中实时显示分贝值。" ) ;
BaselineDbLabel = L ( "settings.study.baseline_db_label" , "基准显示分贝" ) ;
BaselineDbDescription = L ( "settings.study.baseline_db_desc" , "校准后的显示分贝基准值,用于将 dBFS 转换为用户可读的 dB 值。" ) ;
AvgWindowSecLabel = L ( "settings.study.avg_window_label" , "平均时间窗" ) ;
AvgWindowSecDescription = L ( "settings.study.avg_window_desc" , "噪音平滑显示的时间窗口,较大的值会使显示更稳定但响应更慢。" ) ;
FooterHint = L ( "settings.study.footer_hint" , "这些设置将影响自习环境监测组件的行为。" ) ;
UpdateThresholdText ( ) ;
}
2026-03-13 22:20:12 +08:00
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 ; }
}