diff --git a/LanMountainDesktop.PluginSdk/IPluginSettingsService.cs b/LanMountainDesktop.PluginSdk/IPluginSettingsService.cs new file mode 100644 index 0000000..640fd64 --- /dev/null +++ b/LanMountainDesktop.PluginSdk/IPluginSettingsService.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace LanMountainDesktop.PluginSdk; + +public interface IPluginSettingsService +{ + string PluginId { get; } + + IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId); + + T LoadComponentSection(string componentId, string? placementId, string sectionId) where T : new(); + + void SaveComponentSection( + string componentId, + string? placementId, + string sectionId, + T section, + IReadOnlyCollection? changedKeys = null); + + void DeleteComponentSection(string componentId, string? placementId, string sectionId); +} diff --git a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs index 26239f9..4181e66 100644 --- a/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs +++ b/LanMountainDesktop.PluginSdk/PluginDesktopComponentContext.cs @@ -10,7 +10,8 @@ public sealed class PluginDesktopComponentContext IReadOnlyDictionary properties, string componentId, string? placementId, - double cellSize) + double cellSize, + IPluginSettingsService? pluginSettings = null) { ArgumentNullException.ThrowIfNull(manifest); ArgumentException.ThrowIfNullOrWhiteSpace(pluginDirectory); @@ -27,6 +28,7 @@ public sealed class PluginDesktopComponentContext ComponentId = componentId.Trim(); PlacementId = string.IsNullOrWhiteSpace(placementId) ? null : placementId.Trim(); CellSize = Math.Max(1, cellSize); + PluginSettings = pluginSettings; } public PluginManifest Manifest { get; } @@ -45,6 +47,8 @@ public sealed class PluginDesktopComponentContext public double CellSize { get; } + public IPluginSettingsService? PluginSettings { get; } + public T? GetService() { return (T?)Services.GetService(typeof(T)); diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 8148174..166cc3f 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -10,8 +10,10 @@ using Avalonia.Platform; using Avalonia.Threading; using AvaloniaWebView; using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.ViewModels; using LanMountainDesktop.Views; @@ -33,7 +35,7 @@ public partial class App : Application RestartRequested = 2 } - private readonly AppSettingsService _appSettingsService = new(); + private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly LocalizationService _localizationService = new(); private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService(); private bool _exitCleanupCompleted; @@ -50,6 +52,7 @@ public partial class App : Application (Current as App)?._hostApplicationLifecycle; public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService; + public ISettingsFacadeService SettingsFacade => _settingsFacade; public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle; internal void OpenIndependentSettingsModule(string source, string? pageTag = null) @@ -159,7 +162,8 @@ public partial class App : Application try { _pluginRuntimeService?.Dispose(); - _pluginRuntimeService = new PluginRuntimeService(); + _pluginRuntimeService = new PluginRuntimeService(_settingsFacade); + HostSettingsFacadeProvider.BindPluginRuntime(_pluginRuntimeService); _pluginRuntimeService.LoadInstalledPlugins(); } catch (Exception ex) @@ -564,7 +568,7 @@ public partial class App : Application private string L(string key, string fallback) { - var snapshot = _appSettingsService.Load(); + var snapshot = _settingsFacade.Settings.LoadSnapshot(SettingsScope.App); var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode); return _localizationService.GetString(languageCode, key, fallback); } diff --git a/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs b/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs index e0df3b8..d2b5932 100644 --- a/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs +++ b/LanMountainDesktop/ComponentSystem/DesktopComponentRuntimeContext.cs @@ -1,8 +1,9 @@ -using LanMountainDesktop.Services; +using LanMountainDesktop.PluginSdk; namespace LanMountainDesktop.ComponentSystem; public sealed record DesktopComponentRuntimeContext( string ComponentId, string? PlacementId, - IComponentInstanceSettingsStore ComponentSettingsStore); + ISettingsService SettingsService, + IComponentSettingsAccessor ComponentSettingsAccessor); diff --git a/LanMountainDesktop/ComponentSystem/IComponentSettingsStoreAware.cs b/LanMountainDesktop/ComponentSystem/IComponentSettingsStoreAware.cs deleted file mode 100644 index e98500e..0000000 --- a/LanMountainDesktop/ComponentSystem/IComponentSettingsStoreAware.cs +++ /dev/null @@ -1,8 +0,0 @@ -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.ComponentSystem; - -public interface IComponentSettingsStoreAware -{ - void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore); -} diff --git a/LanMountainDesktop/Program.cs b/LanMountainDesktop/Program.cs index f0522fd..f9efbca 100644 --- a/LanMountainDesktop/Program.cs +++ b/LanMountainDesktop/Program.cs @@ -4,7 +4,9 @@ using System.Threading; using System.Threading.Tasks; using Avalonia; using Avalonia.WebView.Desktop; +using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop; @@ -121,7 +123,10 @@ sealed class Program { try { - return AppRenderingModeHelper.Normalize(new AppSettingsService().Load().AppRenderMode); + var snapshot = HostSettingsFacadeProvider.GetOrCreate() + .Settings + .LoadSnapshot(LanMountainDesktop.PluginSdk.SettingsScope.App); + return AppRenderingModeHelper.Normalize(snapshot.AppRenderMode); } catch (Exception ex) { diff --git a/LanMountainDesktop/Services/ComponentLibraryServices.cs b/LanMountainDesktop/Services/ComponentLibraryServices.cs new file mode 100644 index 0000000..d2f1f84 --- /dev/null +++ b/LanMountainDesktop/Services/ComponentLibraryServices.cs @@ -0,0 +1,114 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.Models; +using LanMountainDesktop.Views; +using LanMountainDesktop.Views.Components; + +namespace LanMountainDesktop.Services; + +public sealed record ComponentLibraryCreateContext( + double CellSize, + TimeZoneService TimeZoneService, + IWeatherInfoService WeatherInfoService, + IRecommendationInfoService RecommendationInfoService, + ICalculatorDataService CalculatorDataService, + string? PlacementId = null); + +public interface IComponentLibraryService +{ + IReadOnlyList GetDefinitions(); + + bool TryCreateControl( + string componentId, + ComponentLibraryCreateContext context, + out Control? control, + out Exception? exception); +} + +public interface IComponentLibraryWindowService +{ + void Open(MainWindow window); + + void Close(MainWindow window); + + void Toggle(MainWindow window); +} + +internal sealed class ComponentLibraryService : IComponentLibraryService +{ + private readonly ComponentRegistry _registry; + private readonly DesktopComponentRuntimeRegistry _runtimeRegistry; + + public ComponentLibraryService(ComponentRegistry registry, DesktopComponentRuntimeRegistry runtimeRegistry) + { + _registry = registry; + _runtimeRegistry = runtimeRegistry; + } + + public IReadOnlyList GetDefinitions() + { + return _registry.GetAll().ToArray(); + } + + public bool TryCreateControl( + string componentId, + ComponentLibraryCreateContext context, + out Control? control, + out Exception? exception) + { + control = null; + exception = null; + + if (!_runtimeRegistry.TryGetDescriptor(componentId, out var descriptor)) + { + return false; + } + + try + { + control = descriptor.CreateControl( + context.CellSize, + context.TimeZoneService, + context.WeatherInfoService, + context.RecommendationInfoService, + context.CalculatorDataService, + context.PlacementId); + return true; + } + catch (Exception ex) + { + exception = ex; + return false; + } + } +} + +internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowService +{ + public void Open(MainWindow window) + { + ArgumentNullException.ThrowIfNull(window); + window.OpenComponentLibraryWindowFromService(); + } + + public void Close(MainWindow window) + { + ArgumentNullException.ThrowIfNull(window); + window.CloseComponentLibraryWindowFromService(); + } + + public void Toggle(MainWindow window) + { + ArgumentNullException.ThrowIfNull(window); + if (window.IsComponentLibraryOpenFromService) + { + window.CloseComponentLibraryWindowFromService(); + return; + } + + window.OpenComponentLibraryWindowFromService(); + } +} diff --git a/LanMountainDesktop/Services/ComponentSettingsService.cs b/LanMountainDesktop/Services/ComponentSettingsService.cs index 0d11cd5..545f392 100644 --- a/LanMountainDesktop/Services/ComponentSettingsService.cs +++ b/LanMountainDesktop/Services/ComponentSettingsService.cs @@ -1,40 +1,22 @@ using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; -using System.Text.Json; using LanMountainDesktop.Models; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Services; public sealed class ComponentSettingsService : IComponentInstanceSettingsStore { - private static readonly JsonSerializerOptions SerializerOptions = new() - { - PropertyNameCaseInsensitive = true, - PropertyNamingPolicy = JsonNamingPolicy.CamelCase, - WriteIndented = true - }; - - private static readonly object CacheGate = new(); - private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400); - - private static string? _cachedPath; - private static ComponentSettingsDocumentSnapshot? _cachedSnapshot; - private static DateTime _cachedWriteTimeUtc = DateTime.MinValue; - private static DateTime _lastProbeUtc = DateTime.MinValue; - - private readonly string _settingsPath; - private readonly string _legacyAppSettingsPath; + private const string LegacySectionId = "__legacy__"; + private readonly ISettingsService? _settingsService; + private readonly IComponentStateStore? _stateStore; + private readonly IComponentMessageStore? _messageStore; private string _scopedComponentId = string.Empty; private string _scopedPlacementId = string.Empty; public ComponentSettingsService() - : this(Path.Combine( - Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), - "LanMountainDesktop")) { + _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; } internal ComponentSettingsService(string settingsDirectory) @@ -44,8 +26,9 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore throw new ArgumentException("Settings directory cannot be null or whitespace.", nameof(settingsDirectory)); } - _settingsPath = Path.Combine(settingsDirectory, "component-settings.json"); - _legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json"); + var storage = new SqliteComponentDomainStorage(settingsDirectory); + _stateStore = storage; + _messageStore = storage; } public ComponentSettingsSnapshot Load() @@ -55,19 +38,15 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore return LoadForComponent(_scopedComponentId, _scopedPlacementId); } - try + if (_settingsService is not null) { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - return document.DefaultSettings.Clone(); - } - } - catch (Exception ex) - { - AppLogger.Warn("ComponentSettings", $"Failed to load component settings from '{_settingsPath}'.", ex); - return new ComponentSettingsSnapshot(); + return _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + subjectId: string.Empty, + placementId: null); } + + return _stateStore?.LoadState(componentId: string.Empty, placementId: null) ?? new ComponentSettingsSnapshot(); } public void Save(ComponentSettingsSnapshot snapshot) @@ -78,186 +57,116 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore return; } - var snapshotToPersist = NormalizeSnapshot(snapshot); + if (_settingsService is not null) + { + _settingsService.SaveSnapshot( + SettingsScope.ComponentInstance, + snapshot ?? new ComponentSettingsSnapshot(), + subjectId: string.Empty, + placementId: null); + return; + } - try - { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - document.DefaultSettings = snapshotToPersist; - PersistDocumentLocked(document); - } - } - catch (Exception ex) - { - AppLogger.Warn("ComponentSettings", $"Failed to save default component settings to '{_settingsPath}'.", ex); - } + _stateStore?.SaveState(componentId: string.Empty, placementId: null, snapshot ?? new ComponentSettingsSnapshot()); } public ComponentSettingsSnapshot LoadForComponent(string componentId, string? placementId) { - try + if (_settingsService is not null) { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - var instanceKey = BuildInstanceKey(componentId, placementId); - if (!string.IsNullOrWhiteSpace(instanceKey) && - document.InstanceSettings.TryGetValue(instanceKey, out var snapshot)) - { - return snapshot.Clone(); - } + return _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + subjectId: componentId, + placementId: placementId); + } - return document.DefaultSettings.Clone(); - } - } - catch (Exception ex) - { - AppLogger.Warn( - "ComponentSettings", - $"Failed to load component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); - return new ComponentSettingsSnapshot(); - } + return _stateStore?.LoadState(componentId, placementId) ?? new ComponentSettingsSnapshot(); } public void SaveForComponent(string componentId, string? placementId, ComponentSettingsSnapshot snapshot) { - var normalizedSnapshot = NormalizeSnapshot(snapshot); - var instanceKey = BuildInstanceKey(componentId, placementId); - if (string.IsNullOrWhiteSpace(instanceKey)) + if (_settingsService is not null) { - Save(normalizedSnapshot); + _settingsService.SaveSnapshot( + SettingsScope.ComponentInstance, + snapshot ?? new ComponentSettingsSnapshot(), + subjectId: componentId, + placementId: placementId); return; } - try - { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - document.InstanceSettings[instanceKey] = normalizedSnapshot; - PersistDocumentLocked(document); - } - } - catch (Exception ex) - { - AppLogger.Warn( - "ComponentSettings", - $"Failed to save component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); - } + _stateStore?.SaveState(componentId, placementId, snapshot ?? new ComponentSettingsSnapshot()); } public void DeleteForComponent(string componentId, string? placementId) { - var instanceKey = BuildInstanceKey(componentId, placementId); - if (string.IsNullOrWhiteSpace(instanceKey)) + if (_settingsService is not null) { + _settingsService.SaveSnapshot( + SettingsScope.ComponentInstance, + new ComponentSettingsSnapshot(), + subjectId: componentId, + placementId: placementId); + _settingsService.DeleteSection(SettingsScope.ComponentInstance, componentId, LegacySectionId, placementId); return; } - try - { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - var changed = document.InstanceSettings.Remove(instanceKey); - changed |= document.PluginSettings.Remove(instanceKey); - if (changed) - { - PersistDocumentLocked(document); - } - } - } - catch (Exception ex) - { - AppLogger.Warn( - "ComponentSettings", - $"Failed to delete component settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); - } + _stateStore?.DeleteState(componentId, placementId); } public T LoadPluginSettings(string componentId, string? placementId) where T : new() { - try + if (_settingsService is not null) { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - var instanceKey = BuildInstanceKey(componentId, placementId); - if (string.IsNullOrWhiteSpace(instanceKey) || - !document.PluginSettings.TryGetValue(instanceKey, out var settingsElement)) - { - return new T(); - } + return _settingsService.LoadSection( + SettingsScope.ComponentInstance, + subjectId: componentId, + sectionId: LegacySectionId, + placementId: placementId); + } - return JsonSerializer.Deserialize(settingsElement.GetRawText(), SerializerOptions) ?? new T(); - } - } - catch (Exception ex) + if (_messageStore is SqliteComponentDomainStorage sqliteStorage) { - AppLogger.Warn( - "ComponentSettings", - $"Failed to load plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); - return new T(); + return sqliteStorage.LoadLegacyMessage(componentId, placementId); } + + return new T(); } public void SavePluginSettings(string componentId, string? placementId, T settings) { - var instanceKey = BuildInstanceKey(componentId, placementId); - if (string.IsNullOrWhiteSpace(instanceKey)) + if (_settingsService is not null) { + _settingsService.SaveSection( + SettingsScope.ComponentInstance, + subjectId: componentId, + sectionId: LegacySectionId, + section: settings, + placementId: placementId); return; } - try + if (_messageStore is SqliteComponentDomainStorage sqliteStorage) { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - document.PluginSettings[instanceKey] = JsonSerializer.SerializeToElement(settings, SerializerOptions).Clone(); - PersistDocumentLocked(document); - } - } - catch (Exception ex) - { - AppLogger.Warn( - "ComponentSettings", - $"Failed to save plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); + sqliteStorage.SaveLegacyMessage(componentId, placementId, settings); } } public void DeletePluginSettings(string componentId, string? placementId) { - var instanceKey = BuildInstanceKey(componentId, placementId); - if (string.IsNullOrWhiteSpace(instanceKey)) + if (_settingsService is not null) { + _settingsService.DeleteSection( + SettingsScope.ComponentInstance, + subjectId: componentId, + sectionId: LegacySectionId, + placementId: placementId); return; } - try + if (_messageStore is SqliteComponentDomainStorage sqliteStorage) { - lock (CacheGate) - { - var document = LoadDocumentLocked(); - if (document.PluginSettings.Remove(instanceKey)) - { - PersistDocumentLocked(document); - } - } - } - catch (Exception ex) - { - AppLogger.Warn( - "ComponentSettings", - $"Failed to delete plugin settings. ComponentId={componentId}; PlacementId={placementId}; Path={_settingsPath}", - ex); + sqliteStorage.DeleteLegacyMessage(componentId, placementId); } } @@ -309,385 +218,9 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore } } - private bool TryGetCachedWithoutProbe(DateTime nowUtc, out ComponentSettingsDocumentSnapshot snapshot) + internal static void ResetCacheForTests() { - if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) && - _cachedSnapshot is not null && - nowUtc - _lastProbeUtc < CacheProbeInterval) - { - snapshot = _cachedSnapshot.Clone(); - return true; - } - - snapshot = null!; - return false; - } - - private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out ComponentSettingsDocumentSnapshot snapshot) - { - if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) && - _cachedSnapshot is not null && - writeTimeUtc == _cachedWriteTimeUtc) - { - snapshot = _cachedSnapshot.Clone(); - return true; - } - - snapshot = null!; - return false; - } - - private ComponentSettingsDocumentSnapshot LoadDocumentLocked() - { - var nowUtc = DateTime.UtcNow; - if (TryGetCachedWithoutProbe(nowUtc, out var cached)) - { - return cached; - } - - var hasFile = File.Exists(_settingsPath); - var writeTimeUtc = hasFile - ? File.GetLastWriteTimeUtc(_settingsPath) - : DateTime.MinValue; - - _lastProbeUtc = nowUtc; - if (TryGetCachedAfterProbe(writeTimeUtc, out cached)) - { - return cached; - } - - ComponentSettingsDocumentSnapshot loadedSnapshot; - var loadDetails = ComponentSettingsLoadDetails.Empty; - if (hasFile) - { - loadDetails = LoadSnapshotFromDisk(); - loadedSnapshot = loadDetails.Snapshot; - } - else if (TryLoadLegacySnapshot(out var migratedSnapshot)) - { - loadedSnapshot = new ComponentSettingsDocumentSnapshot - { - DefaultSettings = NormalizeSnapshot(migratedSnapshot) - }; - loadDetails = new ComponentSettingsLoadDetails( - loadedSnapshot, - ComponentSettingsDocumentFormat.LegacySnapshot, - true); - } - else - { - loadedSnapshot = new ComponentSettingsDocumentSnapshot(); - } - - var normalizedSnapshot = NormalizeDocument(loadedSnapshot); - if (loadDetails.ShouldRewriteToCanonical) - { - writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot); - } - - LogLoadDetails(loadDetails.Format, loadDetails.ShouldRewriteToCanonical, normalizedSnapshot); - UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc); - return normalizedSnapshot.Clone(); - } - - private ComponentSettingsLoadDetails LoadSnapshotFromDisk() - { - try - { - var json = File.ReadAllText(_settingsPath); - using var document = JsonDocument.Parse(json); - if (TryGetDocumentFormat(document.RootElement, out var format)) - { - var snapshot = JsonSerializer.Deserialize(json, SerializerOptions); - return new ComponentSettingsLoadDetails( - snapshot ?? new ComponentSettingsDocumentSnapshot(), - format, - format == ComponentSettingsDocumentFormat.PascalCaseDocument); - } - - var legacySnapshot = JsonSerializer.Deserialize(json, SerializerOptions); - return new ComponentSettingsLoadDetails( - new ComponentSettingsDocumentSnapshot - { - DefaultSettings = NormalizeSnapshot(legacySnapshot) - }, - ComponentSettingsDocumentFormat.LegacySnapshot, - true); - } - catch (Exception ex) - { - AppLogger.Warn("ComponentSettings", $"Failed to deserialize component settings from '{_settingsPath}'.", ex); - return ComponentSettingsLoadDetails.Empty; - } - } - - private bool TryLoadLegacySnapshot(out ComponentSettingsSnapshot snapshot) - { - snapshot = new ComponentSettingsSnapshot(); - try - { - if (!File.Exists(_legacyAppSettingsPath)) - { - return false; - } - - var legacyJson = File.ReadAllText(_legacyAppSettingsPath); - var legacy = JsonSerializer.Deserialize(legacyJson, SerializerOptions); - if (legacy is null) - { - return false; - } - - snapshot = new ComponentSettingsSnapshot - { - DailyArtworkMirrorSource = legacy.DailyArtworkMirrorSource, - ImportedClassSchedules = legacy.ImportedClassSchedules ?? [], - ActiveImportedClassScheduleId = legacy.ActiveImportedClassScheduleId ?? string.Empty, - StudyEnvironmentShowDisplayDb = legacy.StudyEnvironmentShowDisplayDb, - StudyEnvironmentShowDbfs = legacy.StudyEnvironmentShowDbfs, - DesktopClockTimeZoneId = legacy.DesktopClockTimeZoneId, - DesktopClockSecondHandMode = legacy.DesktopClockSecondHandMode, - WorldClockTimeZoneIds = legacy.WorldClockTimeZoneIds ?? [], - WorldClockSecondHandMode = legacy.WorldClockSecondHandMode, - CnrDailyNewsAutoRotateEnabled = legacy.CnrDailyNewsAutoRotateEnabled, - CnrDailyNewsAutoRotateIntervalMinutes = legacy.CnrDailyNewsAutoRotateIntervalMinutes, - IfengNewsAutoRefreshEnabled = legacy.IfengNewsAutoRefreshEnabled, - IfengNewsAutoRefreshIntervalMinutes = legacy.IfengNewsAutoRefreshIntervalMinutes, - IfengNewsChannelType = legacy.IfengNewsChannelType, - DailyWordAutoRefreshEnabled = legacy.DailyWordAutoRefreshEnabled, - DailyWordAutoRefreshIntervalMinutes = legacy.DailyWordAutoRefreshIntervalMinutes, - BilibiliHotSearchAutoRefreshEnabled = legacy.BilibiliHotSearchAutoRefreshEnabled, - BilibiliHotSearchAutoRefreshIntervalMinutes = legacy.BilibiliHotSearchAutoRefreshIntervalMinutes, - BaiduHotSearchAutoRefreshEnabled = legacy.BaiduHotSearchAutoRefreshEnabled, - BaiduHotSearchAutoRefreshIntervalMinutes = legacy.BaiduHotSearchAutoRefreshIntervalMinutes, - BaiduHotSearchSourceType = legacy.BaiduHotSearchSourceType, - WeatherAutoRefreshEnabled = legacy.WeatherAutoRefreshEnabled, - WeatherAutoRefreshIntervalMinutes = legacy.WeatherAutoRefreshIntervalMinutes, - Stcn24ForumAutoRefreshEnabled = legacy.Stcn24ForumAutoRefreshEnabled, - Stcn24ForumAutoRefreshIntervalMinutes = legacy.Stcn24ForumAutoRefreshIntervalMinutes, - Stcn24ForumSourceType = legacy.Stcn24ForumSourceType - }; - - return true; - } - catch (Exception ex) - { - AppLogger.Warn("ComponentSettings", $"Failed to migrate legacy component settings from '{_legacyAppSettingsPath}'.", ex); - return false; - } - } - - private void PersistDocumentLocked(ComponentSettingsDocumentSnapshot snapshot) - { - var writeTimeUtc = PersistSnapshotToDisk(snapshot); - UpdateCache(snapshot, writeTimeUtc, DateTime.UtcNow); - } - - private DateTime PersistSnapshotToDisk(ComponentSettingsDocumentSnapshot snapshot) - { - var directory = Path.GetDirectoryName(_settingsPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - var json = JsonSerializer.Serialize(snapshot, SerializerOptions); - File.WriteAllText(_settingsPath, json); - - return File.Exists(_settingsPath) - ? File.GetLastWriteTimeUtc(_settingsPath) - : DateTime.UtcNow; - } - - private static ComponentSettingsSnapshot NormalizeSnapshot(ComponentSettingsSnapshot? snapshot) - { - var normalized = snapshot?.Clone() ?? new ComponentSettingsSnapshot(); - - normalized.DailyArtworkMirrorSource = DailyArtworkMirrorSources.Normalize(normalized.DailyArtworkMirrorSource); - normalized.ImportedClassSchedules = NormalizeImportedSchedules(normalized.ImportedClassSchedules); - normalized.ActiveImportedClassScheduleId = NormalizeActiveScheduleId( - normalized.ActiveImportedClassScheduleId, - normalized.ImportedClassSchedules); - - if (!normalized.StudyEnvironmentShowDisplayDb && !normalized.StudyEnvironmentShowDbfs) - { - normalized.StudyEnvironmentShowDisplayDb = true; - } - - normalized.DesktopClockTimeZoneId = NormalizeDesktopClockTimeZoneId(normalized.DesktopClockTimeZoneId); - normalized.DesktopClockSecondHandMode = ClockSecondHandMode.Normalize(normalized.DesktopClockSecondHandMode); - normalized.WorldClockTimeZoneIds = WorldClockTimeZoneCatalog - .NormalizeTimeZoneIds(normalized.WorldClockTimeZoneIds) - .ToList(); - normalized.WorldClockSecondHandMode = ClockSecondHandMode.Normalize(normalized.WorldClockSecondHandMode); - normalized.CnrDailyNewsAutoRotateIntervalMinutes = NormalizeCnrInterval(normalized.CnrDailyNewsAutoRotateIntervalMinutes); - normalized.IfengNewsAutoRefreshIntervalMinutes = NormalizeIfengNewsInterval(normalized.IfengNewsAutoRefreshIntervalMinutes); - normalized.IfengNewsChannelType = IfengNewsChannelTypes.Normalize(normalized.IfengNewsChannelType); - normalized.DailyWordAutoRefreshIntervalMinutes = NormalizeDailyWordInterval(normalized.DailyWordAutoRefreshIntervalMinutes); - normalized.BilibiliHotSearchAutoRefreshIntervalMinutes = NormalizeBilibiliHotSearchInterval( - normalized.BilibiliHotSearchAutoRefreshIntervalMinutes); - normalized.BaiduHotSearchAutoRefreshIntervalMinutes = NormalizeBaiduHotSearchInterval( - normalized.BaiduHotSearchAutoRefreshIntervalMinutes); - normalized.BaiduHotSearchSourceType = BaiduHotSearchSourceTypes.Normalize(normalized.BaiduHotSearchSourceType); - normalized.WeatherAutoRefreshIntervalMinutes = NormalizeWeatherInterval(normalized.WeatherAutoRefreshIntervalMinutes); - normalized.Stcn24ForumAutoRefreshIntervalMinutes = NormalizeStcn24ForumInterval(normalized.Stcn24ForumAutoRefreshIntervalMinutes); - normalized.Stcn24ForumSourceType = Stcn24ForumSourceTypes.Normalize(normalized.Stcn24ForumSourceType); - - return normalized; - } - - private static ComponentSettingsDocumentSnapshot NormalizeDocument(ComponentSettingsDocumentSnapshot? snapshot) - { - var normalized = snapshot?.Clone() ?? new ComponentSettingsDocumentSnapshot(); - normalized.DefaultSettings = NormalizeSnapshot(normalized.DefaultSettings); - - var instanceSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var pair in normalized.InstanceSettings) - { - var key = NormalizeInstanceKey(pair.Key); - if (string.IsNullOrWhiteSpace(key)) - { - continue; - } - - instanceSettings[key] = NormalizeSnapshot(pair.Value); - } - - var pluginSettings = new Dictionary(StringComparer.OrdinalIgnoreCase); - foreach (var pair in normalized.PluginSettings) - { - var key = NormalizeInstanceKey(pair.Key); - if (string.IsNullOrWhiteSpace(key)) - { - continue; - } - - pluginSettings[key] = pair.Value.Clone(); - } - - normalized.InstanceSettings = instanceSettings; - normalized.PluginSettings = pluginSettings; - return normalized; - } - - private static List NormalizeImportedSchedules( - IReadOnlyList? schedules) - { - if (schedules is null || schedules.Count == 0) - { - return []; - } - - var result = new List(schedules.Count); - var seenIds = new HashSet(StringComparer.OrdinalIgnoreCase); - foreach (var schedule in schedules) - { - if (schedule is null) - { - continue; - } - - var id = schedule.Id?.Trim() ?? string.Empty; - var filePath = schedule.FilePath?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(filePath)) - { - continue; - } - - if (!seenIds.Add(id)) - { - continue; - } - - result.Add(new ImportedClassScheduleSnapshot - { - Id = id, - DisplayName = schedule.DisplayName?.Trim() ?? string.Empty, - FilePath = filePath - }); - } - - return result; - } - - private static string NormalizeActiveScheduleId( - string? activeScheduleId, - IReadOnlyList schedules) - { - var activeId = activeScheduleId?.Trim() ?? string.Empty; - if (schedules.Count == 0) - { - return string.Empty; - } - - if (string.IsNullOrWhiteSpace(activeId)) - { - return schedules[0].Id; - } - - return schedules.Any(item => string.Equals(item.Id, activeId, StringComparison.OrdinalIgnoreCase)) - ? activeId - : schedules[0].Id; - } - - private static string NormalizeDesktopClockTimeZoneId(string? timeZoneId) - { - var normalizedId = string.IsNullOrWhiteSpace(timeZoneId) - ? "China Standard Time" - : timeZoneId.Trim(); - return WorldClockTimeZoneCatalog.ResolveTimeZoneOrLocal(normalizedId).Id; - } - - private static int NormalizeCnrInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 60); - } - - private static int NormalizeDailyWordInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 360); - } - - private static int NormalizeIfengNewsInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 20); - } - - private static int NormalizeBilibiliHotSearchInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 15); - } - - private static int NormalizeBaiduHotSearchInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 15); - } - - private static int NormalizeWeatherInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 12); - } - - private static int NormalizeStcn24ForumInterval(int minutes) - { - return RefreshIntervalCatalog.Normalize(minutes, 20); - } - - private static string BuildInstanceKey(string componentId, string? placementId) - { - var normalizedComponentId = componentId?.Trim() ?? string.Empty; - var normalizedPlacementId = placementId?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(normalizedComponentId) || string.IsNullOrWhiteSpace(normalizedPlacementId)) - { - return string.Empty; - } - - return $"{normalizedComponentId}::{normalizedPlacementId}"; - } - - private static string NormalizeInstanceKey(string? key) - { - return key?.Trim() ?? string.Empty; + // no-op: SQLite storage is directly persisted without in-memory cache. } private bool HasScopedComponentContext() @@ -695,192 +228,4 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore return !string.IsNullOrWhiteSpace(_scopedComponentId) && !string.IsNullOrWhiteSpace(_scopedPlacementId); } - - private void UpdateCache(ComponentSettingsDocumentSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc) - { - _cachedPath = _settingsPath; - _cachedSnapshot = snapshot.Clone(); - _cachedWriteTimeUtc = writeTimeUtc; - _lastProbeUtc = probeTimeUtc; - } - - internal static void ResetCacheForTests() - { - lock (CacheGate) - { - _cachedPath = null; - _cachedSnapshot = null; - _cachedWriteTimeUtc = DateTime.MinValue; - _lastProbeUtc = DateTime.MinValue; - } - } - - private void LogLoadDetails( - ComponentSettingsDocumentFormat format, - bool rewroteToCanonical, - ComponentSettingsDocumentSnapshot snapshot) - { - AppLogger.Info( - "ComponentSettings", - $"Loaded component settings document. Format={format}; RewroteToCanonical={rewroteToCanonical}; " + - $"InstanceSettings={snapshot.InstanceSettings.Count}; PluginSettings={snapshot.PluginSettings.Count}; Path={_settingsPath}"); - } - - private static bool TryGetDocumentFormat( - JsonElement rootElement, - out ComponentSettingsDocumentFormat format) - { - format = ComponentSettingsDocumentFormat.EmptyDocument; - if (rootElement.ValueKind != JsonValueKind.Object) - { - return false; - } - - var hasDocumentProperties = false; - var requiresCanonicalRewrite = false; - foreach (var property in rootElement.EnumerateObject()) - { - if (!IsDocumentPropertyName(property.Name)) - { - continue; - } - - hasDocumentProperties = true; - if (!IsCanonicalDocumentPropertyName(property.Name)) - { - requiresCanonicalRewrite = true; - } - } - - if (!hasDocumentProperties) - { - return false; - } - - format = requiresCanonicalRewrite - ? ComponentSettingsDocumentFormat.PascalCaseDocument - : ComponentSettingsDocumentFormat.CanonicalDocument; - return true; - } - - private static bool IsDocumentPropertyName(string propertyName) - { - return string.Equals(propertyName, "defaultSettings", StringComparison.OrdinalIgnoreCase) || - string.Equals(propertyName, "instanceSettings", StringComparison.OrdinalIgnoreCase) || - string.Equals(propertyName, "pluginSettings", StringComparison.OrdinalIgnoreCase); - } - - private static bool IsCanonicalDocumentPropertyName(string propertyName) - { - return string.Equals(propertyName, "defaultSettings", StringComparison.Ordinal) || - string.Equals(propertyName, "instanceSettings", StringComparison.Ordinal) || - string.Equals(propertyName, "pluginSettings", StringComparison.Ordinal); - } - - private sealed class ComponentSettingsDocumentSnapshot - { - public ComponentSettingsSnapshot DefaultSettings { get; set; } = new(); - - public Dictionary InstanceSettings { get; set; } = - new(StringComparer.OrdinalIgnoreCase); - - public Dictionary PluginSettings { get; set; } = - new(StringComparer.OrdinalIgnoreCase); - - public ComponentSettingsDocumentSnapshot Clone() - { - var clone = new ComponentSettingsDocumentSnapshot - { - DefaultSettings = DefaultSettings?.Clone() ?? new ComponentSettingsSnapshot(), - InstanceSettings = new Dictionary(StringComparer.OrdinalIgnoreCase), - PluginSettings = new Dictionary(StringComparer.OrdinalIgnoreCase) - }; - - foreach (var pair in InstanceSettings) - { - clone.InstanceSettings[pair.Key] = pair.Value?.Clone() ?? new ComponentSettingsSnapshot(); - } - - foreach (var pair in PluginSettings) - { - clone.PluginSettings[pair.Key] = pair.Value.Clone(); - } - - return clone; - } - } - - private sealed class LegacyComponentSettingsSnapshot - { - public string DailyArtworkMirrorSource { get; set; } = DailyArtworkMirrorSources.Overseas; - - public List? ImportedClassSchedules { get; set; } - - public string? ActiveImportedClassScheduleId { get; set; } - - public bool StudyEnvironmentShowDisplayDb { get; set; } = true; - - public bool StudyEnvironmentShowDbfs { get; set; } - - public string DesktopClockTimeZoneId { get; set; } = "China Standard Time"; - - public string DesktopClockSecondHandMode { get; set; } = "Tick"; - - public List? WorldClockTimeZoneIds { get; set; } - - public string WorldClockSecondHandMode { get; set; } = "Tick"; - - public bool CnrDailyNewsAutoRotateEnabled { get; set; } = true; - - public int CnrDailyNewsAutoRotateIntervalMinutes { get; set; } = 60; - - public bool IfengNewsAutoRefreshEnabled { get; set; } = true; - - public int IfengNewsAutoRefreshIntervalMinutes { get; set; } = 20; - - public string IfengNewsChannelType { get; set; } = IfengNewsChannelTypes.Comprehensive; - - public bool DailyWordAutoRefreshEnabled { get; set; } = true; - - public int DailyWordAutoRefreshIntervalMinutes { get; set; } = 360; - - public bool BilibiliHotSearchAutoRefreshEnabled { get; set; } = true; - - public int BilibiliHotSearchAutoRefreshIntervalMinutes { get; set; } = 15; - - public bool BaiduHotSearchAutoRefreshEnabled { get; set; } = true; - - public int BaiduHotSearchAutoRefreshIntervalMinutes { get; set; } = 15; - - public string BaiduHotSearchSourceType { get; set; } = BaiduHotSearchSourceTypes.Official; - - public bool WeatherAutoRefreshEnabled { get; set; } = true; - - public int WeatherAutoRefreshIntervalMinutes { get; set; } = 12; - - public bool Stcn24ForumAutoRefreshEnabled { get; set; } = true; - - public int Stcn24ForumAutoRefreshIntervalMinutes { get; set; } = 20; - - public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated; - } - - private readonly record struct ComponentSettingsLoadDetails( - ComponentSettingsDocumentSnapshot Snapshot, - ComponentSettingsDocumentFormat Format, - bool ShouldRewriteToCanonical) - { - public static ComponentSettingsLoadDetails Empty { get; } = new( - new ComponentSettingsDocumentSnapshot(), - ComponentSettingsDocumentFormat.EmptyDocument, - false); - } - - private enum ComponentSettingsDocumentFormat - { - EmptyDocument, - CanonicalDocument, - PascalCaseDocument, - LegacySnapshot - } } diff --git a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs index 379941e..b97d246 100644 --- a/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs +++ b/LanMountainDesktop/Services/DesktopComponentRegistryFactory.cs @@ -10,6 +10,7 @@ using Avalonia.Media; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem.Extensions; using LanMountainDesktop.PluginSdk; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Services; @@ -114,6 +115,11 @@ public static class DesktopComponentRegistryFactory { try { + var settingsService = contribution.Plugin.Services.GetService(typeof(ISettingsService)) as ISettingsService + ?? HostSettingsFacadeProvider.GetOrCreate().Settings; + var pluginSettings = new PluginScopedSettingsService( + contribution.Plugin.Manifest.Id, + settingsService); var pluginContext = new PluginDesktopComponentContext( contribution.Plugin.Manifest, contribution.Plugin.Context.PluginDirectory, @@ -122,7 +128,8 @@ public static class DesktopComponentRegistryFactory contribution.Plugin.Context.Properties, contribution.Registration.ComponentId, context.PlacementId, - context.CellSize); + context.CellSize, + pluginSettings); return contribution.Registration.ControlFactory(contribution.Plugin.Services, pluginContext); } diff --git a/LanMountainDesktop/Services/DesktopLayoutSettingsService.cs b/LanMountainDesktop/Services/DesktopLayoutSettingsService.cs index 37f11ad..cf9f57e 100644 --- a/LanMountainDesktop/Services/DesktopLayoutSettingsService.cs +++ b/LanMountainDesktop/Services/DesktopLayoutSettingsService.cs @@ -1,251 +1,19 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Text.Json; using LanMountainDesktop.Models; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Services; public sealed class DesktopLayoutSettingsService { - private static readonly JsonSerializerOptions SerializerOptions = new() - { - WriteIndented = true - }; - private static readonly object CacheGate = new(); - private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400); - - private static string? _cachedPath; - private static DesktopLayoutSettingsSnapshot? _cachedSnapshot; - private static DateTime _cachedWriteTimeUtc = DateTime.MinValue; - private static DateTime _lastProbeUtc = DateTime.MinValue; - - private readonly string _settingsPath; - private readonly string _legacyAppSettingsPath; - - public DesktopLayoutSettingsService() - { - var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var settingsDirectory = Path.Combine(appData, "LanMountainDesktop"); - _settingsPath = Path.Combine(settingsDirectory, "desktop-layout-settings.json"); - _legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json"); - } + private readonly IComponentLayoutStore _layoutStore = ComponentDomainStorageProvider.Instance; public DesktopLayoutSettingsSnapshot Load() { - try - { - lock (CacheGate) - { - var nowUtc = DateTime.UtcNow; - if (TryGetCachedWithoutProbe(nowUtc, out var cached)) - { - return cached; - } - - var hasFile = File.Exists(_settingsPath); - var writeTimeUtc = hasFile - ? File.GetLastWriteTimeUtc(_settingsPath) - : DateTime.MinValue; - - _lastProbeUtc = nowUtc; - if (TryGetCachedAfterProbe(writeTimeUtc, out cached)) - { - return cached; - } - - DesktopLayoutSettingsSnapshot loadedSnapshot; - var loadedFromLegacy = false; - if (hasFile) - { - loadedSnapshot = LoadSnapshotFromDisk(); - } - else if (TryLoadLegacySnapshot(out var migratedSnapshot)) - { - loadedSnapshot = migratedSnapshot; - loadedFromLegacy = true; - } - else - { - loadedSnapshot = new DesktopLayoutSettingsSnapshot(); - } - - var normalizedSnapshot = NormalizeSnapshot(loadedSnapshot); - if (loadedFromLegacy) - { - writeTimeUtc = PersistSnapshotToDisk(normalizedSnapshot); - } - - UpdateCache(normalizedSnapshot, writeTimeUtc, nowUtc); - return normalizedSnapshot.Clone(); - } - } - catch (Exception ex) - { - AppLogger.Warn("DesktopLayout", $"Failed to load desktop layout settings from '{_settingsPath}'.", ex); - return new DesktopLayoutSettingsSnapshot(); - } + return _layoutStore.LoadLayout(); } public void Save(DesktopLayoutSettingsSnapshot snapshot) { - var snapshotToPersist = NormalizeSnapshot(snapshot); - - try - { - var writeTimeUtc = PersistSnapshotToDisk(snapshotToPersist); - - lock (CacheGate) - { - UpdateCache(snapshotToPersist, writeTimeUtc, DateTime.UtcNow); - } - } - catch (Exception ex) - { - AppLogger.Warn("DesktopLayout", $"Failed to save desktop layout settings to '{_settingsPath}'.", ex); - } - } - - private bool TryGetCachedWithoutProbe(DateTime nowUtc, out DesktopLayoutSettingsSnapshot snapshot) - { - if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) && - _cachedSnapshot is not null && - nowUtc - _lastProbeUtc < CacheProbeInterval) - { - snapshot = _cachedSnapshot.Clone(); - return true; - } - - snapshot = null!; - return false; - } - - private bool TryGetCachedAfterProbe(DateTime writeTimeUtc, out DesktopLayoutSettingsSnapshot snapshot) - { - if (string.Equals(_cachedPath, _settingsPath, StringComparison.Ordinal) && - _cachedSnapshot is not null && - writeTimeUtc == _cachedWriteTimeUtc) - { - snapshot = _cachedSnapshot.Clone(); - return true; - } - - snapshot = null!; - return false; - } - - private DesktopLayoutSettingsSnapshot LoadSnapshotFromDisk() - { - try - { - var json = File.ReadAllText(_settingsPath); - var snapshot = JsonSerializer.Deserialize(json, SerializerOptions); - return NormalizeSnapshot(snapshot); - } - catch (Exception ex) - { - AppLogger.Warn("DesktopLayout", $"Failed to deserialize desktop layout settings from '{_settingsPath}'.", ex); - return new DesktopLayoutSettingsSnapshot(); - } - } - - private bool TryLoadLegacySnapshot(out DesktopLayoutSettingsSnapshot snapshot) - { - snapshot = new DesktopLayoutSettingsSnapshot(); - - try - { - if (!File.Exists(_legacyAppSettingsPath)) - { - return false; - } - - var legacyJson = File.ReadAllText(_legacyAppSettingsPath); - var legacy = JsonSerializer.Deserialize(legacyJson, SerializerOptions); - if (legacy is null) - { - return false; - } - - snapshot = new DesktopLayoutSettingsSnapshot - { - DesktopPageCount = legacy.DesktopPageCount, - CurrentDesktopSurfaceIndex = legacy.CurrentDesktopSurfaceIndex, - DesktopComponentPlacements = legacy.DesktopComponentPlacements ?? [] - }; - - return true; - } - catch (Exception ex) - { - AppLogger.Warn("DesktopLayout", $"Failed to migrate legacy desktop layout settings from '{_legacyAppSettingsPath}'.", ex); - return false; - } - } - - private DateTime PersistSnapshotToDisk(DesktopLayoutSettingsSnapshot snapshot) - { - var directory = Path.GetDirectoryName(_settingsPath); - if (!string.IsNullOrWhiteSpace(directory)) - { - Directory.CreateDirectory(directory); - } - - var json = JsonSerializer.Serialize(snapshot, SerializerOptions); - File.WriteAllText(_settingsPath, json); - - return File.Exists(_settingsPath) - ? File.GetLastWriteTimeUtc(_settingsPath) - : DateTime.UtcNow; - } - - private static DesktopLayoutSettingsSnapshot NormalizeSnapshot(DesktopLayoutSettingsSnapshot? snapshot) - { - var normalized = snapshot?.Clone() ?? new DesktopLayoutSettingsSnapshot(); - normalized.DesktopPageCount = Math.Max(1, normalized.DesktopPageCount); - normalized.CurrentDesktopSurfaceIndex = Math.Max(0, normalized.CurrentDesktopSurfaceIndex); - - var placements = new List(normalized.DesktopComponentPlacements?.Count ?? 0); - if (normalized.DesktopComponentPlacements is not null) - { - foreach (var placement in normalized.DesktopComponentPlacements) - { - if (placement is null) - { - continue; - } - - placements.Add(new DesktopComponentPlacementSnapshot - { - PlacementId = placement.PlacementId?.Trim() ?? string.Empty, - PageIndex = Math.Max(0, placement.PageIndex), - ComponentId = placement.ComponentId?.Trim() ?? string.Empty, - Row = Math.Max(0, placement.Row), - Column = Math.Max(0, placement.Column), - WidthCells = Math.Max(1, placement.WidthCells), - HeightCells = Math.Max(1, placement.HeightCells) - }); - } - } - - normalized.DesktopComponentPlacements = placements; - return normalized; - } - - private void UpdateCache(DesktopLayoutSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc) - { - _cachedPath = _settingsPath; - _cachedSnapshot = snapshot.Clone(); - _cachedWriteTimeUtc = writeTimeUtc; - _lastProbeUtc = probeTimeUtc; - } - - private sealed class LegacyDesktopLayoutSettingsSnapshot - { - public int DesktopPageCount { get; set; } = 1; - - public int CurrentDesktopSurfaceIndex { get; set; } - - public List? DesktopComponentPlacements { get; set; } + _layoutStore.SaveLayout(snapshot ?? new DesktopLayoutSettingsSnapshot()); } } diff --git a/LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs b/LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs new file mode 100644 index 0000000..13b608a --- /dev/null +++ b/LanMountainDesktop/Services/Settings/ComponentDomainStorage.cs @@ -0,0 +1,845 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.IO; +using System.Text.Json; +using LanMountainDesktop.Models; +using Microsoft.Data.Sqlite; + +namespace LanMountainDesktop.Services.Settings; + +public interface IComponentLayoutStore +{ + DesktopLayoutSettingsSnapshot LoadLayout(); + + void SaveLayout(DesktopLayoutSettingsSnapshot snapshot); +} + +public interface IComponentStateStore +{ + ComponentSettingsSnapshot LoadState(string componentId, string? placementId); + + void SaveState(string componentId, string? placementId, ComponentSettingsSnapshot snapshot); + + void DeleteState(string componentId, string? placementId); +} + +public interface IComponentMessageStore +{ + T LoadSection(string componentId, string? placementId, string sectionId) where T : new(); + + void SaveSection(string componentId, string? placementId, string sectionId, T section); + + void DeleteSection(string componentId, string? placementId, string sectionId); +} + +internal static class ComponentDomainStorageProvider +{ + private static readonly object Gate = new(); + private static SqliteComponentDomainStorage? _instance; + + public static SqliteComponentDomainStorage Instance + { + get + { + lock (Gate) + { + _instance ??= new SqliteComponentDomainStorage(); + return _instance; + } + } + } +} + +internal sealed class SqliteComponentDomainStorage : + IComponentLayoutStore, + IComponentStateStore, + IComponentMessageStore +{ + private const string MigrationMarkerKey = "component_domain_v1"; + private const string DefaultInstanceKey = "__default__"; + private const string LegacySectionId = "__legacy__"; + + private static readonly JsonSerializerOptions SerializerOptions = new() + { + PropertyNameCaseInsensitive = true, + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }; + + private readonly object _gate = new(); + private readonly string _settingsRoot; + private readonly string _dbPath; + private readonly string _layoutJsonPath; + private readonly string _componentJsonPath; + + public SqliteComponentDomainStorage(string? settingsRoot = null) + { + _settingsRoot = string.IsNullOrWhiteSpace(settingsRoot) + ? Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop") + : settingsRoot.Trim(); + _dbPath = Path.Combine(_settingsRoot, "component-state.db"); + _layoutJsonPath = Path.Combine(_settingsRoot, "desktop-layout-settings.json"); + _componentJsonPath = Path.Combine(_settingsRoot, "component-settings.json"); + + Directory.CreateDirectory(_settingsRoot); + InitializeDatabase(); + } + + public DesktopLayoutSettingsSnapshot LoadLayout() + { + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT desktop_page_count, current_desktop_surface_index + FROM component_layout + WHERE id = 1; + """; + using var reader = command.ExecuteReader(); + if (!reader.Read()) + { + return new DesktopLayoutSettingsSnapshot(); + } + + return new DesktopLayoutSettingsSnapshot + { + DesktopPageCount = Math.Max(1, reader.GetInt32(0)), + CurrentDesktopSurfaceIndex = Math.Max(0, reader.GetInt32(1)), + DesktopComponentPlacements = LoadPlacements(connection) + }; + } + } + + public void SaveLayout(DesktopLayoutSettingsSnapshot snapshot) + { + var normalized = snapshot?.Clone() ?? new DesktopLayoutSettingsSnapshot(); + normalized.DesktopPageCount = Math.Max(1, normalized.DesktopPageCount); + normalized.CurrentDesktopSurfaceIndex = Math.Max(0, normalized.CurrentDesktopSurfaceIndex); + + lock (_gate) + { + using var connection = OpenConnection(); + using var transaction = connection.BeginTransaction(); + + using (var command = connection.CreateCommand()) + { + command.Transaction = transaction; + command.CommandText = """ + INSERT INTO component_layout(id, desktop_page_count, current_desktop_surface_index, updated_utc) + VALUES(1, $count, $index, $updated) + ON CONFLICT(id) DO UPDATE SET + desktop_page_count = excluded.desktop_page_count, + current_desktop_surface_index = excluded.current_desktop_surface_index, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$count", normalized.DesktopPageCount); + command.Parameters.AddWithValue("$index", normalized.CurrentDesktopSurfaceIndex); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + + using (var deleteCommand = connection.CreateCommand()) + { + deleteCommand.Transaction = transaction; + deleteCommand.CommandText = "DELETE FROM component_placement;"; + deleteCommand.ExecuteNonQuery(); + } + + if (normalized.DesktopComponentPlacements is { Count: > 0 }) + { + foreach (var placement in normalized.DesktopComponentPlacements) + { + if (placement is null || string.IsNullOrWhiteSpace(placement.PlacementId)) + { + continue; + } + + using var insertCommand = connection.CreateCommand(); + insertCommand.Transaction = transaction; + insertCommand.CommandText = """ + INSERT INTO component_placement( + placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells, updated_utc) + VALUES($placementId, $page, $componentId, $row, $column, $width, $height, $updated); + """; + insertCommand.Parameters.AddWithValue("$placementId", placement.PlacementId.Trim()); + insertCommand.Parameters.AddWithValue("$page", Math.Max(0, placement.PageIndex)); + insertCommand.Parameters.AddWithValue("$componentId", placement.ComponentId?.Trim() ?? string.Empty); + insertCommand.Parameters.AddWithValue("$row", Math.Max(0, placement.Row)); + insertCommand.Parameters.AddWithValue("$column", Math.Max(0, placement.Column)); + insertCommand.Parameters.AddWithValue("$width", Math.Max(1, placement.WidthCells)); + insertCommand.Parameters.AddWithValue("$height", Math.Max(1, placement.HeightCells)); + insertCommand.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + insertCommand.ExecuteNonQuery(); + } + } + + transaction.Commit(); + } + } + + public ComponentSettingsSnapshot LoadState(string componentId, string? placementId) + { + var instanceKey = BuildInstanceKey(componentId, placementId); + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT state_json + FROM component_state + WHERE instance_key = $instanceKey + LIMIT 1; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + var json = command.ExecuteScalar() as string; + if (string.IsNullOrWhiteSpace(json)) + { + if (string.Equals(instanceKey, DefaultInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + return new ComponentSettingsSnapshot(); + } + + return LoadDefaultState(connection); + } + + return DeserializeState(json); + } + } + + public void SaveState(string componentId, string? placementId, ComponentSettingsSnapshot snapshot) + { + var instanceKey = BuildInstanceKey(componentId, placementId); + var normalizedComponentId = NormalizeKey(componentId); + var normalizedPlacementId = NormalizePlacement(placementId); + var json = JsonSerializer.Serialize(snapshot ?? new ComponentSettingsSnapshot(), SerializerOptions); + + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + INSERT INTO component_state(instance_key, component_id, placement_id, state_json, updated_utc) + VALUES($instanceKey, $componentId, $placementId, $stateJson, $updated) + ON CONFLICT(instance_key) DO UPDATE SET + component_id = excluded.component_id, + placement_id = excluded.placement_id, + state_json = excluded.state_json, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + command.Parameters.AddWithValue("$componentId", normalizedComponentId); + command.Parameters.AddWithValue("$placementId", normalizedPlacementId); + command.Parameters.AddWithValue("$stateJson", json); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + } + + public void DeleteState(string componentId, string? placementId) + { + var instanceKey = BuildInstanceKey(componentId, placementId); + if (string.Equals(instanceKey, DefaultInstanceKey, StringComparison.OrdinalIgnoreCase)) + { + return; + } + + lock (_gate) + { + using var connection = OpenConnection(); + using var transaction = connection.BeginTransaction(); + + using (var stateDelete = connection.CreateCommand()) + { + stateDelete.Transaction = transaction; + stateDelete.CommandText = "DELETE FROM component_state WHERE instance_key = $instanceKey;"; + stateDelete.Parameters.AddWithValue("$instanceKey", instanceKey); + stateDelete.ExecuteNonQuery(); + } + + using (var messageDelete = connection.CreateCommand()) + { + messageDelete.Transaction = transaction; + messageDelete.CommandText = "DELETE FROM component_message WHERE instance_key = $instanceKey;"; + messageDelete.Parameters.AddWithValue("$instanceKey", instanceKey); + messageDelete.ExecuteNonQuery(); + } + + transaction.Commit(); + } + } + + public T LoadSection(string componentId, string? placementId, string sectionId) where T : new() + { + var instanceKey = BuildInstanceKey(componentId, placementId); + var normalizedSectionId = NormalizeSection(sectionId); + + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT message_json + FROM component_message + WHERE instance_key = $instanceKey + AND section_id = $sectionId + LIMIT 1; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + command.Parameters.AddWithValue("$sectionId", normalizedSectionId); + var json = command.ExecuteScalar() as string; + if (string.IsNullOrWhiteSpace(json)) + { + return new T(); + } + + try + { + return JsonSerializer.Deserialize(json, SerializerOptions) ?? new T(); + } + catch + { + return new T(); + } + } + } + + public void SaveSection(string componentId, string? placementId, string sectionId, T section) + { + var instanceKey = BuildInstanceKey(componentId, placementId); + var normalizedComponentId = NormalizeKey(componentId); + var normalizedPlacementId = NormalizePlacement(placementId); + var normalizedSectionId = NormalizeSection(sectionId); + var json = JsonSerializer.Serialize(section, SerializerOptions); + + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + INSERT INTO component_message(instance_key, component_id, placement_id, section_id, message_json, updated_utc) + VALUES($instanceKey, $componentId, $placementId, $sectionId, $messageJson, $updated) + ON CONFLICT(instance_key, section_id) DO UPDATE SET + component_id = excluded.component_id, + placement_id = excluded.placement_id, + message_json = excluded.message_json, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + command.Parameters.AddWithValue("$componentId", normalizedComponentId); + command.Parameters.AddWithValue("$placementId", normalizedPlacementId); + command.Parameters.AddWithValue("$sectionId", normalizedSectionId); + command.Parameters.AddWithValue("$messageJson", json); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + } + + public void DeleteSection(string componentId, string? placementId, string sectionId) + { + var instanceKey = BuildInstanceKey(componentId, placementId); + var normalizedSectionId = NormalizeSection(sectionId); + + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + DELETE FROM component_message + WHERE instance_key = $instanceKey + AND section_id = $sectionId; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + command.Parameters.AddWithValue("$sectionId", normalizedSectionId); + command.ExecuteNonQuery(); + } + } + + public T LoadLegacyMessage(string componentId, string? placementId) where T : new() + { + return LoadSection(componentId, placementId, LegacySectionId); + } + + public void SaveLegacyMessage(string componentId, string? placementId, T section) + { + SaveSection(componentId, placementId, LegacySectionId, section); + } + + public void DeleteLegacyMessage(string componentId, string? placementId) + { + DeleteSection(componentId, placementId, LegacySectionId); + } + + private void InitializeDatabase() + { + lock (_gate) + { + using var connection = OpenConnection(); + using var command = connection.CreateCommand(); + command.CommandText = """ + CREATE TABLE IF NOT EXISTS settings_meta( + key TEXT PRIMARY KEY, + value TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS component_layout( + id INTEGER PRIMARY KEY CHECK(id = 1), + desktop_page_count INTEGER NOT NULL, + current_desktop_surface_index INTEGER NOT NULL, + updated_utc TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS component_placement( + placement_id TEXT PRIMARY KEY, + page_index INTEGER NOT NULL, + component_id TEXT NOT NULL, + row_index INTEGER NOT NULL, + column_index INTEGER NOT NULL, + width_cells INTEGER NOT NULL, + height_cells INTEGER NOT NULL, + updated_utc TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS component_state( + instance_key TEXT PRIMARY KEY, + component_id TEXT NOT NULL, + placement_id TEXT NOT NULL, + state_json TEXT NOT NULL, + updated_utc TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS component_message( + instance_key TEXT NOT NULL, + component_id TEXT NOT NULL, + placement_id TEXT NOT NULL, + section_id TEXT NOT NULL, + message_json TEXT NOT NULL, + updated_utc TEXT NOT NULL, + PRIMARY KEY(instance_key, section_id) + ); + """; + command.ExecuteNonQuery(); + + if (!IsMigrationApplied(connection)) + { + ApplyInitialMigration(connection); + } + } + } + + private bool IsMigrationApplied(SqliteConnection connection) + { + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT value + FROM settings_meta + WHERE key = $key + LIMIT 1; + """; + command.Parameters.AddWithValue("$key", MigrationMarkerKey); + var raw = command.ExecuteScalar() as string; + return string.Equals(raw, "applied", StringComparison.OrdinalIgnoreCase); + } + + private void ApplyInitialMigration(SqliteConnection connection) + { + AppLogger.Info("ComponentDomainStorage", "Starting one-shot migration from legacy JSON files to SQLite."); + using var transaction = connection.BeginTransaction(); + try + { + if (TryLoadLegacyLayout(out var layout)) + { + PersistLayout(connection, transaction, layout); + } + + if (TryLoadLegacyComponentDocument(out var document)) + { + PersistComponentDocument(connection, transaction, document); + } + + using var markerCommand = connection.CreateCommand(); + markerCommand.Transaction = transaction; + markerCommand.CommandText = """ + INSERT INTO settings_meta(key, value) + VALUES($key, 'applied') + ON CONFLICT(key) DO UPDATE SET value = 'applied'; + """; + markerCommand.Parameters.AddWithValue("$key", MigrationMarkerKey); + markerCommand.ExecuteNonQuery(); + + transaction.Commit(); + BackupLegacyFile(_layoutJsonPath); + BackupLegacyFile(_componentJsonPath); + AppLogger.Info("ComponentDomainStorage", "Legacy JSON migration completed."); + } + catch (Exception ex) + { + transaction.Rollback(); + AppLogger.Error("ComponentDomainStorage", "Legacy JSON migration failed. SQLite writes are blocked for this session.", ex); + throw; + } + } + + private void PersistLayout( + SqliteConnection connection, + SqliteTransaction transaction, + DesktopLayoutSettingsSnapshot snapshot) + { + using (var command = connection.CreateCommand()) + { + command.Transaction = transaction; + command.CommandText = """ + INSERT INTO component_layout(id, desktop_page_count, current_desktop_surface_index, updated_utc) + VALUES(1, $count, $index, $updated) + ON CONFLICT(id) DO UPDATE SET + desktop_page_count = excluded.desktop_page_count, + current_desktop_surface_index = excluded.current_desktop_surface_index, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$count", Math.Max(1, snapshot.DesktopPageCount)); + command.Parameters.AddWithValue("$index", Math.Max(0, snapshot.CurrentDesktopSurfaceIndex)); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + + if (snapshot.DesktopComponentPlacements is not { Count: > 0 }) + { + return; + } + + foreach (var placement in snapshot.DesktopComponentPlacements) + { + if (placement is null || string.IsNullOrWhiteSpace(placement.PlacementId)) + { + continue; + } + + using var placementCommand = connection.CreateCommand(); + placementCommand.Transaction = transaction; + placementCommand.CommandText = """ + INSERT INTO component_placement( + placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells, updated_utc) + VALUES($placementId, $page, $componentId, $row, $column, $width, $height, $updated) + ON CONFLICT(placement_id) DO UPDATE SET + page_index = excluded.page_index, + component_id = excluded.component_id, + row_index = excluded.row_index, + column_index = excluded.column_index, + width_cells = excluded.width_cells, + height_cells = excluded.height_cells, + updated_utc = excluded.updated_utc; + """; + placementCommand.Parameters.AddWithValue("$placementId", placement.PlacementId.Trim()); + placementCommand.Parameters.AddWithValue("$page", Math.Max(0, placement.PageIndex)); + placementCommand.Parameters.AddWithValue("$componentId", placement.ComponentId?.Trim() ?? string.Empty); + placementCommand.Parameters.AddWithValue("$row", Math.Max(0, placement.Row)); + placementCommand.Parameters.AddWithValue("$column", Math.Max(0, placement.Column)); + placementCommand.Parameters.AddWithValue("$width", Math.Max(1, placement.WidthCells)); + placementCommand.Parameters.AddWithValue("$height", Math.Max(1, placement.HeightCells)); + placementCommand.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + placementCommand.ExecuteNonQuery(); + } + } + + private void PersistComponentDocument( + SqliteConnection connection, + SqliteTransaction transaction, + LegacyComponentDocument document) + { + PersistComponentState(connection, transaction, DefaultInstanceKey, "__default__", string.Empty, document.DefaultSettings ?? new ComponentSettingsSnapshot()); + + if (document.InstanceSettings is not null) + { + foreach (var pair in document.InstanceSettings) + { + if (!TrySplitInstanceKey(pair.Key, out var componentId, out var placementId)) + { + continue; + } + + PersistComponentState(connection, transaction, pair.Key.Trim(), componentId, placementId, pair.Value ?? new ComponentSettingsSnapshot()); + } + } + + if (document.PluginSettings is null) + { + return; + } + + foreach (var pair in document.PluginSettings) + { + if (!TrySplitInstanceKey(pair.Key, out var componentId, out var placementId)) + { + continue; + } + + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = """ + INSERT INTO component_message(instance_key, component_id, placement_id, section_id, message_json, updated_utc) + VALUES($instanceKey, $componentId, $placementId, $sectionId, $json, $updated) + ON CONFLICT(instance_key, section_id) DO UPDATE SET + component_id = excluded.component_id, + placement_id = excluded.placement_id, + message_json = excluded.message_json, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$instanceKey", pair.Key.Trim()); + command.Parameters.AddWithValue("$componentId", componentId); + command.Parameters.AddWithValue("$placementId", placementId); + command.Parameters.AddWithValue("$sectionId", LegacySectionId); + command.Parameters.AddWithValue("$json", pair.Value.GetRawText()); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + } + + private static void PersistComponentState( + SqliteConnection connection, + SqliteTransaction transaction, + string instanceKey, + string componentId, + string placementId, + ComponentSettingsSnapshot snapshot) + { + var json = JsonSerializer.Serialize(snapshot ?? new ComponentSettingsSnapshot(), SerializerOptions); + using var command = connection.CreateCommand(); + command.Transaction = transaction; + command.CommandText = """ + INSERT INTO component_state(instance_key, component_id, placement_id, state_json, updated_utc) + VALUES($instanceKey, $componentId, $placementId, $stateJson, $updated) + ON CONFLICT(instance_key) DO UPDATE SET + component_id = excluded.component_id, + placement_id = excluded.placement_id, + state_json = excluded.state_json, + updated_utc = excluded.updated_utc; + """; + command.Parameters.AddWithValue("$instanceKey", instanceKey); + command.Parameters.AddWithValue("$componentId", componentId); + command.Parameters.AddWithValue("$placementId", placementId); + command.Parameters.AddWithValue("$stateJson", json); + command.Parameters.AddWithValue("$updated", DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture)); + command.ExecuteNonQuery(); + } + + private bool TryLoadLegacyLayout(out DesktopLayoutSettingsSnapshot snapshot) + { + snapshot = new DesktopLayoutSettingsSnapshot(); + if (!File.Exists(_layoutJsonPath)) + { + return false; + } + + try + { + var json = File.ReadAllText(_layoutJsonPath); + snapshot = JsonSerializer.Deserialize(json, SerializerOptions) ?? new DesktopLayoutSettingsSnapshot(); + return true; + } + catch (Exception ex) + { + AppLogger.Warn("ComponentDomainStorage", $"Failed to read legacy layout file '{_layoutJsonPath}'.", ex); + return false; + } + } + + private bool TryLoadLegacyComponentDocument(out LegacyComponentDocument document) + { + document = new LegacyComponentDocument(); + if (!File.Exists(_componentJsonPath)) + { + return false; + } + + try + { + var json = File.ReadAllText(_componentJsonPath); + using var parsed = JsonDocument.Parse(json); + if (parsed.RootElement.ValueKind != JsonValueKind.Object) + { + return false; + } + + var hasDocumentShape = false; + foreach (var property in parsed.RootElement.EnumerateObject()) + { + if (string.Equals(property.Name, "defaultSettings", StringComparison.OrdinalIgnoreCase) || + string.Equals(property.Name, "instanceSettings", StringComparison.OrdinalIgnoreCase) || + string.Equals(property.Name, "pluginSettings", StringComparison.OrdinalIgnoreCase)) + { + hasDocumentShape = true; + break; + } + } + + if (hasDocumentShape) + { + document = JsonSerializer.Deserialize(json, SerializerOptions) ?? new LegacyComponentDocument(); + document.DefaultSettings ??= new ComponentSettingsSnapshot(); + document.InstanceSettings ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + document.PluginSettings ??= new Dictionary(StringComparer.OrdinalIgnoreCase); + return true; + } + + var legacySingle = JsonSerializer.Deserialize(json, SerializerOptions) ?? new ComponentSettingsSnapshot(); + document = new LegacyComponentDocument + { + DefaultSettings = legacySingle + }; + return true; + } + catch (Exception ex) + { + AppLogger.Warn("ComponentDomainStorage", $"Failed to read legacy component settings file '{_componentJsonPath}'.", ex); + return false; + } + } + + private static void BackupLegacyFile(string path) + { + if (!File.Exists(path)) + { + return; + } + + try + { + var backupPath = $"{path}.migrated.bak"; + if (File.Exists(backupPath)) + { + File.Delete(backupPath); + } + + File.Move(path, backupPath); + } + catch (Exception ex) + { + AppLogger.Warn("ComponentDomainStorage", $"Failed to backup migrated legacy file '{path}'.", ex); + } + } + + private static bool TrySplitInstanceKey(string key, out string componentId, out string placementId) + { + componentId = string.Empty; + placementId = string.Empty; + if (string.IsNullOrWhiteSpace(key)) + { + return false; + } + + var normalized = key.Trim(); + var parts = normalized.Split("::", 2, StringSplitOptions.TrimEntries); + if (parts.Length != 2 || + string.IsNullOrWhiteSpace(parts[0]) || + string.IsNullOrWhiteSpace(parts[1])) + { + return false; + } + + componentId = parts[0]; + placementId = parts[1]; + return true; + } + + private static string BuildInstanceKey(string componentId, string? placementId) + { + var normalizedComponentId = NormalizeKey(componentId); + var normalizedPlacementId = NormalizePlacement(placementId); + if (string.IsNullOrWhiteSpace(normalizedComponentId) || + string.IsNullOrWhiteSpace(normalizedPlacementId)) + { + return DefaultInstanceKey; + } + + return $"{normalizedComponentId}::{normalizedPlacementId}"; + } + + private static string NormalizeKey(string? key) + { + return key?.Trim() ?? string.Empty; + } + + private static string NormalizePlacement(string? placementId) + { + return placementId?.Trim() ?? string.Empty; + } + + private static string NormalizeSection(string? sectionId) + { + return string.IsNullOrWhiteSpace(sectionId) ? LegacySectionId : sectionId.Trim(); + } + + private static ComponentSettingsSnapshot DeserializeState(string json) + { + try + { + return JsonSerializer.Deserialize(json, SerializerOptions) ?? new ComponentSettingsSnapshot(); + } + catch + { + return new ComponentSettingsSnapshot(); + } + } + + private static ComponentSettingsSnapshot LoadDefaultState(SqliteConnection connection) + { + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT state_json + FROM component_state + WHERE instance_key = $instanceKey + LIMIT 1; + """; + command.Parameters.AddWithValue("$instanceKey", DefaultInstanceKey); + var json = command.ExecuteScalar() as string; + return string.IsNullOrWhiteSpace(json) ? new ComponentSettingsSnapshot() : DeserializeState(json); + } + + private static List LoadPlacements(SqliteConnection connection) + { + var placements = new List(); + using var command = connection.CreateCommand(); + command.CommandText = """ + SELECT placement_id, page_index, component_id, row_index, column_index, width_cells, height_cells + FROM component_placement + ORDER BY page_index, row_index, column_index; + """; + using var reader = command.ExecuteReader(); + while (reader.Read()) + { + placements.Add(new DesktopComponentPlacementSnapshot + { + PlacementId = reader.IsDBNull(0) ? string.Empty : reader.GetString(0), + PageIndex = reader.IsDBNull(1) ? 0 : Math.Max(0, reader.GetInt32(1)), + ComponentId = reader.IsDBNull(2) ? string.Empty : reader.GetString(2), + Row = reader.IsDBNull(3) ? 0 : Math.Max(0, reader.GetInt32(3)), + Column = reader.IsDBNull(4) ? 0 : Math.Max(0, reader.GetInt32(4)), + WidthCells = reader.IsDBNull(5) ? 1 : Math.Max(1, reader.GetInt32(5)), + HeightCells = reader.IsDBNull(6) ? 1 : Math.Max(1, reader.GetInt32(6)) + }); + } + + return placements; + } + + private SqliteConnection OpenConnection() + { + var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadWriteCreate;Cache=Shared"); + connection.Open(); + return connection; + } + + private sealed class LegacyComponentDocument + { + public ComponentSettingsSnapshot? DefaultSettings { get; set; } = new(); + + public Dictionary? InstanceSettings { get; set; } = + new(StringComparer.OrdinalIgnoreCase); + + public Dictionary? PluginSettings { get; set; } = + new(StringComparer.OrdinalIgnoreCase); + } +} diff --git a/LanMountainDesktop/Services/Settings/HostSettingsFacadeProvider.cs b/LanMountainDesktop/Services/Settings/HostSettingsFacadeProvider.cs new file mode 100644 index 0000000..ed57266 --- /dev/null +++ b/LanMountainDesktop/Services/Settings/HostSettingsFacadeProvider.cs @@ -0,0 +1,28 @@ +using System; + +namespace LanMountainDesktop.Services.Settings; + +internal static class HostSettingsFacadeProvider +{ + private static readonly object Gate = new(); + private static SettingsFacadeService? _instance; + + public static ISettingsFacadeService GetOrCreate() + { + lock (Gate) + { + _instance ??= new SettingsFacadeService(); + return _instance; + } + } + + public static void BindPluginRuntime(PluginRuntimeService pluginRuntimeService) + { + ArgumentNullException.ThrowIfNull(pluginRuntimeService); + lock (Gate) + { + _instance ??= new SettingsFacadeService(pluginRuntimeService); + _instance.BindPluginRuntime(pluginRuntimeService); + } + } +} diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs index 0b75104..278593c 100644 --- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs +++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs @@ -74,6 +74,15 @@ public interface IGridSettingsService { GridSettingsState Get(); void Save(GridSettingsState state); + string NormalizeSpacingPreset(string? value); + double ResolveGapRatio(string? preset); + double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent); + DesktopGridMetrics CalculateGridMetrics( + double hostWidth, + double hostHeight, + int shortSideCells, + double gapRatio, + double edgeInsetPx); } public interface IWallpaperSettingsService @@ -117,12 +126,14 @@ public interface IWeatherSettingsService { WeatherSettingsState Get(); void Save(WeatherSettingsState state); + IWeatherInfoService GetWeatherInfoService(); } public interface IRegionSettingsService { RegionSettingsState Get(); void Save(RegionSettingsState state); + TimeZoneService GetTimeZoneService(); } public interface IUpdateSettingsService diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs index 40f942d..5f35e30 100644 --- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs +++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs @@ -14,6 +14,7 @@ namespace LanMountainDesktop.Services.Settings; internal sealed class GridSettingsService : IGridSettingsService { private readonly AppSettingsService _appSettingsService = new(); + private readonly DesktopGridLayoutService _gridLayoutService = new(); public GridSettingsState Get() { @@ -32,6 +33,36 @@ internal sealed class GridSettingsService : IGridSettingsService snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent; _appSettingsService.Save(snapshot); } + + public string NormalizeSpacingPreset(string? value) + { + return _gridLayoutService.NormalizeSpacingPreset(value); + } + + public double ResolveGapRatio(string? preset) + { + return _gridLayoutService.ResolveGapRatio(preset); + } + + public double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent) + { + return _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, shortSideCells, insetPercent); + } + + public DesktopGridMetrics CalculateGridMetrics( + double hostWidth, + double hostHeight, + int shortSideCells, + double gapRatio, + double edgeInsetPx) + { + return _gridLayoutService.CalculateGridMetrics( + hostWidth, + hostHeight, + shortSideCells, + gapRatio, + edgeInsetPx); + } } internal sealed class WallpaperSettingsService : IWallpaperSettingsService @@ -234,7 +265,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService } } -internal sealed class WeatherProviderAdapter : IWeatherProvider +internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoService, IDisposable { private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); @@ -252,11 +283,20 @@ internal sealed class WeatherProviderAdapter : IWeatherProvider { return _weatherDataService.GetWeatherAsync(query, cancellationToken); } + + public void Dispose() + { + if (_weatherDataService is IDisposable disposable) + { + disposable.Dispose(); + } + } } -internal sealed class WeatherSettingsService : IWeatherSettingsService +internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposable { private readonly AppSettingsService _appSettingsService = new(); + private readonly WeatherProviderAdapter _weatherProvider = new(); public WeatherSettingsState Get() { @@ -289,11 +329,22 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService snapshot.WeatherLocationQuery = state.LocationQuery; _appSettingsService.Save(snapshot); } + + public IWeatherInfoService GetWeatherInfoService() + { + return _weatherProvider; + } + + public void Dispose() + { + _weatherProvider.Dispose(); + } } internal sealed class RegionSettingsService : IRegionSettingsService { private readonly AppSettingsService _appSettingsService = new(); + private readonly TimeZoneService _timeZoneService = new(); public RegionSettingsState Get() { @@ -312,6 +363,11 @@ internal sealed class RegionSettingsService : IRegionSettingsService : state.TimeZoneId.Trim(); _appSettingsService.Save(snapshot); } + + public TimeZoneService GetTimeZoneService() + { + return _timeZoneService; + } } internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable @@ -388,13 +444,18 @@ internal sealed class LauncherPolicyService : ILauncherPolicyService internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService { private readonly AppSettingsService _appSettingsService = new(); - private readonly PluginRuntimeService? _pluginRuntimeService; + private PluginRuntimeService? _pluginRuntimeService; public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService) { _pluginRuntimeService = pluginRuntimeService; } + public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService) + { + _pluginRuntimeService = pluginRuntimeService; + } + public PluginManagementSettingsState Get() { var snapshot = _appSettingsService.Load(); @@ -426,9 +487,9 @@ internal sealed class PluginManagementSettingsService : IPluginManagementSetting internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable { - private readonly PluginRuntimeService? _pluginRuntimeService; - private readonly AirAppMarketIndexService _indexService; - private readonly AirAppMarketInstallService? _installService; + private PluginRuntimeService? _pluginRuntimeService; + private AirAppMarketIndexService _indexService; + private AirAppMarketInstallService? _installService; private readonly Dictionary _cachedPlugins = new(StringComparer.OrdinalIgnoreCase); public PluginMarketSettingsService(PluginRuntimeService? pluginRuntimeService) @@ -447,6 +508,24 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService } } + public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService) + { + _pluginRuntimeService = pluginRuntimeService; + _installService?.Dispose(); + _installService = null; + + if (_pluginRuntimeService is null) + { + return; + } + + var dataRoot = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "LanMountainDesktop", + "PluginMarket"); + _installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot); + } + public async Task LoadIndexAsync(CancellationToken cancellationToken = default) { var result = await _indexService.LoadAsync(cancellationToken); @@ -567,6 +646,8 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl { private readonly UpdateSettingsService _updateSettingsService; private readonly PluginMarketSettingsService _pluginMarketSettingsService; + private readonly PluginManagementSettingsService _pluginManagementSettingsService; + private readonly WeatherSettingsService _weatherSettingsService; public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null) { @@ -577,13 +658,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl WallpaperMedia = new WallpaperMediaService(); Theme = new ThemeAppearanceService(); StatusBar = new StatusBarSettingsService(); - Weather = new WeatherSettingsService(); + _weatherSettingsService = new WeatherSettingsService(); + Weather = _weatherSettingsService; Region = new RegionSettingsService(); _updateSettingsService = new UpdateSettingsService(); Update = _updateSettingsService; LauncherCatalog = new LauncherCatalogService(); LauncherPolicy = new LauncherPolicyService(); - PluginManagement = new PluginManagementSettingsService(pluginRuntimeService); + _pluginManagementSettingsService = new PluginManagementSettingsService(pluginRuntimeService); + PluginManagement = _pluginManagementSettingsService; _pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService); PluginMarket = _pluginMarketSettingsService; ApplicationInfo = new ApplicationInfoService(); @@ -619,8 +702,15 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl public IApplicationInfoService ApplicationInfo { get; } + public void BindPluginRuntime(PluginRuntimeService? pluginRuntimeService) + { + _pluginManagementSettingsService.SetPluginRuntime(pluginRuntimeService); + _pluginMarketSettingsService.SetPluginRuntime(pluginRuntimeService); + } + public void Dispose() { + _weatherSettingsService.Dispose(); _updateSettingsService.Dispose(); _pluginMarketSettingsService.Dispose(); } diff --git a/LanMountainDesktop/Services/Settings/SettingsService.cs b/LanMountainDesktop/Services/Settings/SettingsService.cs index fc5487e..9f091c7 100644 --- a/LanMountainDesktop/Services/Settings/SettingsService.cs +++ b/LanMountainDesktop/Services/Settings/SettingsService.cs @@ -19,7 +19,8 @@ internal sealed class SettingsService : ISettingsService private readonly AppSettingsService _appSettingsService = new(); private readonly LauncherSettingsService _launcherSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); + private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance; + private readonly IComponentMessageStore _componentMessageStore = ComponentDomainStorageProvider.Instance; private readonly string _pluginSettingsPath; private readonly object _pluginSettingsGate = new(); @@ -80,7 +81,7 @@ internal sealed class SettingsService : ISettingsService { if (scope == SettingsScope.ComponentInstance) { - return _componentSettingsService.LoadPluginSettings(EnsureKey(subjectId), placementId); + return _componentMessageStore.LoadSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId)); } if (scope != SettingsScope.Plugin) @@ -111,7 +112,7 @@ internal sealed class SettingsService : ISettingsService { if (scope == SettingsScope.ComponentInstance) { - _componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, section); + _componentMessageStore.SaveSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId), section); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys)); return; } @@ -142,7 +143,7 @@ internal sealed class SettingsService : ISettingsService { if (scope == SettingsScope.ComponentInstance) { - _componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId); + _componentMessageStore.DeleteSection(EnsureKey(subjectId), placementId, EnsureKey(sectionId)); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId)); return; } @@ -183,7 +184,11 @@ internal sealed class SettingsService : ISettingsService SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions), SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions), SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement( - _componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId), + LoadSection>( + SettingsScope.ComponentInstance, + EnsureKey(subjectId), + sectionId ?? "__root__", + placementId), SerializerOptions), SettingsScope.Plugin => JsonSerializer.SerializeToElement( LoadSection>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId), @@ -239,9 +244,10 @@ internal sealed class SettingsService : ISettingsService if (scope == SettingsScope.ComponentInstance) { - var dict = _componentSettingsService.LoadPluginSettings>(EnsureKey(subjectId), placementId); + var effectiveSection = sectionId ?? "__root__"; + var dict = _componentMessageStore.LoadSection>(EnsureKey(subjectId), placementId, effectiveSection); dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone(); - _componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict); + _componentMessageStore.SaveSection(EnsureKey(subjectId), placementId, effectiveSection, dict); OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key])); return; } @@ -271,14 +277,14 @@ internal sealed class SettingsService : ISettingsService private T LoadComponentSnapshot(string? componentId, string? placementId) where T : new() { - var snapshot = _componentSettingsService.LoadForComponent(EnsureKey(componentId), placementId); + var snapshot = _componentStateStore.LoadState(EnsureKey(componentId), placementId); return ConvertSnapshot(snapshot); } private void SaveComponentSnapshot(string? componentId, string? placementId, T snapshot) { var converted = ConvertSnapshot(snapshot); - _componentSettingsService.SaveForComponent(EnsureKey(componentId), placementId, converted); + _componentStateStore.SaveState(EnsureKey(componentId), placementId, converted); } private static TOut ConvertSnapshot(TIn source) where TOut : new() diff --git a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs index 3ed9940..801fa4d 100644 --- a/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/AnalogClockWidget.axaml.cs @@ -9,10 +9,11 @@ using Avalonia.Styling; using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Views.Components; -public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware { private static readonly IReadOnlyDictionary ZhCityNames = new Dictionary(StringComparer.OrdinalIgnoreCase) @@ -58,8 +59,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I private string _componentId = BuiltInComponentIds.DesktopClock; private string _placementId = string.Empty; - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); + private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; private readonly LocalizationService _localizationService = new(); private TimeZoneService? _timeZoneService; private double _currentCellSize = 48; @@ -124,12 +124,6 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { InitializeDialIfNeeded(); @@ -376,8 +370,11 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I private void LoadClockSettings() { - var appSnapshot = _appSettingsService.Load(); - var componentSnapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); + var appSnapshot = _settingsService.LoadSnapshot(SettingsScope.App); + var componentSnapshot = _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + _componentId, + _placementId); _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode); var configuredTimeZoneId = string.IsNullOrWhiteSpace(componentSnapshot.DesktopClockTimeZoneId) diff --git a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs index fef59bb..bb68223 100644 --- a/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ClassScheduleWidget.axaml.cs @@ -10,10 +10,11 @@ using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Views.Components; -public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware { private sealed record CourseItemViewModel( string Name, @@ -26,8 +27,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, Interval = TimeSpan.FromMinutes(4) }; - private readonly AppSettingsService _appSettingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); + private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; private readonly LocalizationService _localizationService = new(); private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService(); @@ -125,16 +125,13 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget, RefreshSchedule(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshSchedule(); - } - private void RefreshSchedule() { - var appSettings = _appSettingsService.Load(); - var componentSettings = _componentSettingsStore.LoadForComponent(_componentId, _placementId); + var appSettings = _settingsService.LoadSnapshot(SettingsScope.App); + var componentSettings = _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + _componentId, + _placementId); _languageCode = _localizationService.NormalizeLanguageCode(appSettings.LanguageCode); var now = _timeZoneService?.GetCurrentTime() ?? DateTime.Now; UpdateHeader(now); diff --git a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs index 99eeebf..d4d5f3b 100644 --- a/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/DailyArtworkWidget.axaml.cs @@ -16,10 +16,11 @@ using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Views.Components; -public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget, IComponentPlacementContextAware { private static readonly IReadOnlyDictionary ZhWeekdays = new Dictionary @@ -58,8 +59,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, Interval = TimeSpan.FromHours(6) }; - private readonly AppSettingsService _settingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); + private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; private readonly LocalizationService _localizationService = new(); private IRecommendationInfoService _recommendationService = DefaultRecommendationService; @@ -149,12 +149,6 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) { _isAttached = true; @@ -651,7 +645,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, { try { - var snapshot = _settingsService.Load(); + var snapshot = _settingsService.LoadSnapshot(SettingsScope.App); _languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode); } catch @@ -664,7 +658,10 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget, { try { - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); + var snapshot = _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + _componentId, + _placementId); return DailyArtworkMirrorSources.Normalize(snapshot.DailyArtworkMirrorSource); } catch diff --git a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs index 2c3cf62..0f20ecd 100644 --- a/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs +++ b/LanMountainDesktop/Views/Components/DesktopComponentRuntimeRegistry.cs @@ -3,7 +3,9 @@ using System.Collections.Generic; using System.Linq; using Avalonia.Controls; using LanMountainDesktop.ComponentSystem; +using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; namespace LanMountainDesktop.Views.Components; @@ -14,7 +16,8 @@ public sealed record DesktopComponentControlFactoryContext( IWeatherInfoService WeatherInfoService, IRecommendationInfoService RecommendationInfoService, ICalculatorDataService CalculatorDataService, - IComponentInstanceSettingsStore ComponentSettingsStore, + ISettingsService SettingsService, + IComponentSettingsAccessor ComponentSettingsAccessor, string? PlacementId = null); public sealed class DesktopComponentRuntimeRegistration @@ -84,9 +87,10 @@ public sealed class DesktopComponentRuntimeDescriptor IWeatherInfoService weatherInfoService, IRecommendationInfoService recommendationInfoService, ICalculatorDataService calculatorDataService, - IComponentInstanceSettingsStore componentSettingsStore, string? placementId = null) { + var settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; + var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId); var control = _controlFactory(new DesktopComponentControlFactoryContext( Definition, cellSize, @@ -94,12 +98,14 @@ public sealed class DesktopComponentRuntimeDescriptor weatherInfoService, recommendationInfoService, calculatorDataService, - componentSettingsStore, + settingsService, + componentAccessor, placementId)); var runtimeContext = new DesktopComponentRuntimeContext( Definition.Id, placementId, - componentSettingsStore); + settingsService, + componentAccessor); if (control is IComponentRuntimeContextAware runtimeContextAwareComponent) { @@ -111,13 +117,6 @@ public sealed class DesktopComponentRuntimeDescriptor placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId); } - if (control is IComponentSettingsStoreAware settingsStoreAwareComponent) - { - settingsStoreAwareComponent.SetComponentSettingsStore(componentSettingsStore); - } - - ComponentSettingsService.ApplyScopedContextToTarget(control, Definition.Id, placementId); - if (control is IDesktopComponentWidget sizedComponent) { sizedComponent.ApplyCellSize(cellSize); diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs index 22e0fb2..66e702d 100644 --- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs @@ -12,11 +12,12 @@ using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; -public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware { private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes; @@ -25,8 +26,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval }; private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1); private readonly TranslateTransform _backgroundMotionTranslateTransform = new(); - private readonly AppSettingsService _settingsService = new(); - private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); + private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings; private readonly LocalizationService _localizationService = new(); private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService; @@ -185,12 +185,6 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) { _ = isEditMode; @@ -265,7 +259,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge } _isRefreshing = true; - var app = _settingsService.Load(); + var app = _settingsService.LoadSnapshot(SettingsScope.App); _languageCode = _localizationService.NormalizeLanguageCode(app.LanguageCode); var locale = string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) ? "zh_cn" : "en_us"; var latitude = double.IsFinite(app.WeatherLatitude) ? Math.Clamp(app.WeatherLatitude, -90, 90) : 39.9042; @@ -953,7 +947,10 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge try { - var snapshot = _componentSettingsStore.LoadForComponent(_componentId, _placementId); + var snapshot = _settingsService.LoadSnapshot( + SettingsScope.ComponentInstance, + _componentId, + _placementId); enabled = snapshot.WeatherAutoRefreshEnabled; intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes); } diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs index c2bb96f..ebd6c62 100644 --- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs @@ -18,7 +18,7 @@ using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; -public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware { private enum WeatherVisualKind { @@ -236,12 +236,6 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget, RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) { _ = isEditMode; diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs index 53ee724..3e9d43e 100644 --- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs @@ -16,7 +16,7 @@ using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; -public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware { private enum WeatherVisualKind { @@ -234,12 +234,6 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) { _ = isEditMode; diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs index bed0d1a..88e6a58 100644 --- a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs @@ -16,7 +16,7 @@ using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; -public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware { private sealed record WeatherClockConfig( string LanguageCode, @@ -128,12 +128,6 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget, RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void ApplyCellSize(double cellSize) { _currentCellSize = Math.Max(1, cellSize); diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs index 754a3a9..a53e6bf 100644 --- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs @@ -18,7 +18,7 @@ using LanMountainDesktop.Theme; namespace LanMountainDesktop.Views.Components; -public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget, IComponentPlacementContextAware { private enum WeatherVisualKind { @@ -179,12 +179,6 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode) { _ = isEditMode; diff --git a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs index a3e5971..ae7b2d3 100644 --- a/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/WorldClockWidget.axaml.cs @@ -13,7 +13,7 @@ using LanMountainDesktop.Services; namespace LanMountainDesktop.Views.Components; -public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware, IComponentSettingsStoreAware +public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, ITimeZoneAwareComponentWidget, IComponentPlacementContextAware { private const int BaseWidthCells = 4; private const int BaseHeightCells = 2; @@ -159,12 +159,6 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT RefreshFromSettings(); } - public void SetComponentSettingsStore(IComponentInstanceSettingsStore settingsStore) - { - _componentSettingsStore = settingsStore ?? new ComponentSettingsService(); - RefreshFromSettings(); - } - public void ApplyCellSize(double cellSize) { _currentCellSize = Math.Max(1, cellSize); diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 1dbce4b..a8c1826 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -101,19 +101,12 @@ public partial class MainWindow private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) { - // "Desktop edit" toggle. While editing, show the component library window. - if (_isComponentLibraryOpen) - { - CloseComponentLibraryWindow(reopenSettings: false); - return; - } - - OpenComponentLibraryWindow(); + _componentLibraryWindowService.Toggle(this); } private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e) { - CloseComponentLibraryWindow(reopenSettings: false); + _componentLibraryWindowService.Close(this); } private void OnCloseComponentSettingsClick(object? sender, RoutedEventArgs e) @@ -202,21 +195,6 @@ public partial class MainWindow { ClockWidget.SetDisplayFormat(_clockDisplayFormat); } - - if (_clockDisplayFormat == ClockDisplayFormat.HourMinute) - { - if (ClockFormatHMRadio is not null) - { - ClockFormatHMRadio.IsChecked = true; - } - } - else - { - if (ClockFormatHMSSRadio is not null) - { - ClockFormatHMSSRadio.IsChecked = true; - } - } } private void ApplyTopStatusComponentVisibility() @@ -233,15 +211,6 @@ public partial class MainWindow Grid.SetColumnSpan(ClockWidget, columnSpan); } } - - if (WallpaperPreviewClockWidget is not null) - { - WallpaperPreviewClockWidget.IsVisible = showClock; - if (showClock) - { - WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat); - } - } } private TaskbarContext GetCurrentTaskbarContext() @@ -251,11 +220,7 @@ public partial class MainWindow private void ApplyTaskbarActionVisibility(TaskbarContext context) { - if (BackToWindowsButton is null || - OpenComponentLibraryButton is null || - WallpaperPreviewBackButtonVisual is null || - WallpaperPreviewComponentLibraryVisual is null || - WallpaperPreviewSettingsButtonIcon is null) + if (BackToWindowsButton is null || OpenComponentLibraryButton is null) { return; } @@ -266,9 +231,6 @@ public partial class MainWindow BackToWindowsButton.IsVisible = showMinimize; OpenComponentLibraryButton.IsVisible = showDesktopEdit; - WallpaperPreviewBackButtonVisual.IsVisible = showMinimize; - WallpaperPreviewComponentLibraryVisual.IsVisible = showDesktopEdit; - WallpaperPreviewSettingsButtonIcon.IsVisible = showSettings; if (TaskbarFixedActionsHost is not null) { @@ -280,16 +242,6 @@ public partial class MainWindow TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; } - if (WallpaperPreviewTaskbarFixedActionsHost is not null) - { - WallpaperPreviewTaskbarFixedActionsHost.IsVisible = showMinimize; - } - - if (WallpaperPreviewTaskbarSettingsActionHost is not null) - { - WallpaperPreviewTaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; - } - var dynamicActions = ResolveDynamicTaskbarActions(context) .Where(action => action.IsVisible) .ToList(); @@ -300,11 +252,6 @@ public partial class MainWindow { TaskbarDynamicActionsHost.IsVisible = hasDynamicActions; } - - if (WallpaperPreviewTaskbarDynamicActionsHost is not null) - { - WallpaperPreviewTaskbarDynamicActionsHost.IsVisible = hasDynamicActions; - } } private void UpdateOpenSettingsActionVisualState() @@ -455,14 +402,7 @@ public partial class MainWindow TaskbarDynamicActionsPanel.Children.Clear(); } - if (WallpaperPreviewTaskbarDynamicActionsHost is not null) - { - WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear(); - } - - if (actions.Count == 0 || - TaskbarDynamicActionsPanel is null || - WallpaperPreviewTaskbarDynamicActionsHost is null) + if (actions.Count == 0 || TaskbarDynamicActionsPanel is null) { return; } @@ -556,37 +496,6 @@ public partial class MainWindow TaskbarDynamicActionsPanel.Children.Add(button); - Control previewIcon = new SymbolIcon - { - Symbol = iconSymbol, - IconVariant = IconVariant.Regular, - FontSize = iconSize * 0.85 - }; - - var previewText = new TextBlock - { - Text = action.Title, - FontSize = fontSize * 0.85, - Foreground = (isDeleteAction || isHideAction) - ? new SolidColorBrush(Color.Parse("#FFFF6B6B")) - : Foreground, - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center - }; - - var previewContent = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = spacing * 0.5, - Children = { previewIcon, previewText } - }; - - var previewBorder = new Border - { - Background = Brushes.Transparent, - BorderThickness = new Thickness(0), - Child = previewContent - }; - WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder); } } @@ -637,7 +546,7 @@ public partial class MainWindow } _desktopComponentPlacements.Remove(placement); - _componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId); + _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId); ClearDesktopComponentSelection(); @@ -698,7 +607,7 @@ public partial class MainWindow foreach (var placement in placementsToRemove) { _desktopComponentPlacements.Remove(placement); - _componentSettingsService.DeleteForComponent(placement.ComponentId, placement.PlacementId); + _componentSettingsStore.DeleteForComponent(placement.ComponentId, placement.PlacementId); } _desktopPageCount = Math.Clamp(_desktopPageCount - 1, MinDesktopPageCount, MaxDesktopPageCount); @@ -1345,14 +1254,19 @@ public partial class MainWindow { try { - var component = runtimeDescriptor.CreateControl( + var createContext = new ComponentLibraryCreateContext( cellSize, _timeZoneService, _weatherDataService, _recommendationInfoService, _calculatorDataService, - _componentSettingsService, placementId); + if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) || + component is null) + { + throw exception ?? new InvalidOperationException("Component library service returned no control."); + } + component.Classes.Add(DesktopComponentClass); return component; } @@ -1376,6 +1290,18 @@ public partial class MainWindow } } + internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen; + + internal void OpenComponentLibraryWindowFromService() + { + OpenComponentLibraryWindow(); + } + + internal void CloseComponentLibraryWindowFromService() + { + CloseComponentLibraryWindow(reopenSettings: false); + } + private void CollapseComponentLibraryPanel() { // Animate component library panel collapsing downward diff --git a/LanMountainDesktop/Views/MainWindow.Localization.cs b/LanMountainDesktop/Views/MainWindow.Localization.cs deleted file mode 100644 index d8d5671..0000000 --- a/LanMountainDesktop/Views/MainWindow.Localization.cs +++ /dev/null @@ -1,456 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Layout; -using FluentIcons.Avalonia; -using FluentIcons.Common; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views; - -public partial class MainWindow -{ - private const string AppCodeName = "Administrate"; - private const string AppFontName = "MiSans"; - private const string FallbackAppVersion = "1.0.0"; - - private static readonly IReadOnlyDictionary ZhTimeZoneNames = - new Dictionary(StringComparer.OrdinalIgnoreCase) - { - ["China Standard Time"] = "中国标准时间", - ["Asia/Shanghai"] = "中国标准时间", - ["Tokyo Standard Time"] = "日本标准时间", - ["Asia/Tokyo"] = "日本标准时间", - ["Pacific Standard Time"] = "太平洋标准时间", - ["America/Los_Angeles"] = "太平洋标准时间", - ["Eastern Standard Time"] = "美国东部标准时间", - ["America/New_York"] = "美国东部标准时间", - ["Central European Standard Time"] = "中欧标准时间", - ["Europe/Berlin"] = "中欧标准时间", - ["GMT Standard Time"] = "格林威治标准时间", - ["Europe/London"] = "格林威治标准时间", - ["UTC"] = "协调世界时", - ["Etc/UTC"] = "协调世界时" - }; - - private void InitializeLocalization(string? languageCode) - { - _languageCode = _localizationService.NormalizeLanguageCode(languageCode); - - if (LanguageComboBox is null) - { - return; - } - - _suppressLanguageSelectionEvents = true; - LanguageComboBox.SelectedIndex = string.Equals(_languageCode, "en-US", StringComparison.OrdinalIgnoreCase) ? 1 : 0; - _suppressLanguageSelectionEvents = false; - } - - private string L(string key, string fallback) - { - return _localizationService.GetString(_languageCode, key, fallback); - } - - private string Lf(string key, string fallback, params object[] args) - { - var template = L(key, fallback); - return string.Format(template, args); - } - - private string GetLanguageDisplayName(string languageCode) - { - return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase) - ? L("settings.region.language_en", "English") - : L("settings.region.language_zh", "Chinese"); - } - - private string GetLocalizedPlacementDisplayName(WallpaperPlacement placement) - { - return placement switch - { - WallpaperPlacement.Fill => L("placement.fill", "Fill"), - WallpaperPlacement.Fit => L("placement.fit", "Fit"), - WallpaperPlacement.Stretch => L("placement.stretch", "Stretch"), - WallpaperPlacement.Center => L("placement.center", "Center"), - WallpaperPlacement.Tile => L("placement.tile", "Tile"), - _ => L("placement.fill", "Fill") - }; - } - - private void ApplyLocalization() - { - Title = L("app.title", "LanMountainDesktop"); - - BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows"); - WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows"); - ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows")); - - OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); - WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); - GridPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); - ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Edit Desktop")); - ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Widgets"); - ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close")); - ComponentLibraryEmptyTextBlock.Text = L( - "component_library.empty", - "Swipe to pick a category, tap to open, then drag a widget onto the desktop."); - - LauncherTitleTextBlock.Text = L("launcher.title", "App Launcher"); - LauncherSubtitleTextBlock.Text = OperatingSystem.IsLinux() - ? L( - "launcher.subtitle_linux", - "Displays installed apps discovered from Linux desktop entries.") - : L( - "launcher.subtitle", - "Displays all apps and folders based on the Windows Start menu structure."); - ToolTip.SetTip(LauncherFolderBackButton, L("common.back", "Back")); - ToolTip.SetTip(LauncherFolderCloseButton, L("common.close", "Close")); - - // SettingsNavHeaderTextBlock no longer exists - SettingsNavWallpaperItem.Content = L("settings.nav.wallpaper", "Wallpaper"); - SettingsNavGridItem.Content = L("settings.nav.grid", "Grid"); - SettingsNavColorItem.Content = L("settings.nav.color", "Color"); - SettingsNavStatusBarItem.Content = L("settings.nav.status_bar", "Status Bar"); - SettingsNavWeatherItem.Content = L("settings.nav.weather", "Weather"); - SettingsNavRegionItem.Content = L("settings.nav.region", "Region"); - SettingsNavUpdateItem.Content = L("settings.nav.update", "Update"); - SettingsNavLauncherItem.Content = L("settings.nav.launcher", "App Launcher"); - SettingsNavPluginsItem.Content = L("settings.nav.plugins", "Plugins"); - SettingsNavPluginMarketItem.Content = L("settings.nav.plugin_market", "Plugin Market"); - - WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Personalize your wallpaper"); - WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement"); - WallpaperPlacementSettingsExpander.Description = L( - "settings.wallpaper.placement_desc", - "Adjust how the image fits on the desktop."); - PickWallpaperButton.Content = L("settings.wallpaper.pick_button", "Browse"); - ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "Reset"); - - GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout"); - GridSpacingSettingsExpander.Header = L("settings.grid.spacing_label", "Grid Spacing"); - GridSpacingRelaxedComboBoxItem.Content = L("settings.grid.spacing_relaxed", "Relaxed"); - GridSpacingCompactComboBoxItem.Content = L("settings.grid.spacing_compact", "Compact"); - GridEdgeInsetSettingsExpander.Header = L("settings.grid.edge_inset_label", "Screen Inset"); - ApplyGridButton.Content = L("settings.grid.apply_button", "Apply"); - UpdateGridEdgeInsetComputedPxText(_currentDesktopCellSize); - - ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color"); - ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night"); - NightModeToggleSwitch.OffContent = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = 6, - Children = - { - new SymbolIcon { Symbol = Symbol.WeatherSunny, IconVariant = IconVariant.Regular, FontSize = 14 }, - new TextBlock - { - Text = L("settings.color.day_night_off", "Day"), - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center - } - } - }; - NightModeToggleSwitch.OnContent = new StackPanel - { - Orientation = Orientation.Horizontal, - Spacing = 6, - Children = - { - new SymbolIcon { Symbol = Symbol.WeatherMoon, IconVariant = IconVariant.Regular, FontSize = 14 }, - new TextBlock - { - Text = L("settings.color.day_night_on", "Night"), - VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center - } - } - }; - RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors"); - SystemMonetColorsLabelTextBlock.Text = L("settings.color.system_monet_label", "System Monet Colors"); - RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh"); - - StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar"); - StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock"); - StatusBarSpacingSettingsExpander.Header = L("settings.status_bar.spacing_header", "Component Spacing"); - StatusBarSpacingSettingsExpander.Description = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components."); - StatusBarSpacingModeCompactItem.Content = L("settings.status_bar.spacing_mode_compact", "Compact"); - StatusBarSpacingModeRelaxedItem.Content = L("settings.status_bar.spacing_mode_relaxed", "Relaxed"); - StatusBarSpacingModeCustomItem.Content = L("settings.status_bar.spacing_mode_custom", "Custom"); - StatusBarSpacingCustomPanel.Content = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)"); - - WeatherPanelTitleTextBlock.Text = L("settings.weather.title", "Weather"); - WeatherPreviewSectionTextBlock.Text = L("settings.weather.preview_section", "Weather Preview"); - WeatherSettingsSectionTextBlock.Text = L("settings.weather.settings_section", "Settings"); - WeatherPreviewSettingsExpander.Header = L("settings.weather.preview_panel_header", "Weather Preview"); - WeatherPreviewSettingsExpander.Description = L( - "settings.weather.preview_panel_desc", - "Refresh and verify current weather service status."); - WeatherPreviewButton.Content = L("settings.weather.refresh_button", "Refresh"); - - WeatherLocationSettingsExpander.Header = L("settings.weather.location_source_header", "Location Source"); - WeatherLocationSettingsExpander.Description = L( - "settings.weather.location_source_desc", - "Choose how weather widgets resolve location."); - WeatherLocationModeCityItem.Content = L("settings.weather.mode_city_search", "City Search"); - WeatherLocationModeCoordinatesItem.Content = L("settings.weather.mode_coordinates", "Coordinates"); - WeatherLocationModeCityChipItem.Content = L("settings.weather.mode_city_search", "City Search"); - WeatherLocationModeCoordinatesChipItem.Content = L("settings.weather.mode_coordinates", "Coordinates"); - WeatherAutoRefreshToggleSwitch.Content = L("settings.weather.auto_refresh", "Auto refresh location on startup"); - WeatherLocationSelectionTitleTextBlock.Text = L("settings.weather.city_selection_label", "City Selection"); - WeatherLocationSelectionDescriptionTextBlock.Text = L( - "settings.weather.location_city_summary_desc", - "Select the current city used for weather queries."); - - WeatherCitySearchSettingsExpander.Header = L("settings.weather.city_search_header", "City Search"); - WeatherCitySearchSettingsExpander.Description = L( - "settings.weather.city_search_desc", - "Search cities and apply one weather location."); - WeatherCitySearchTextBox.Watermark = L("settings.weather.search_placeholder", "e.g. Beijing"); - WeatherSearchButton.Content = L("settings.weather.search_button", "Search"); - WeatherApplyCityButton.Content = L("settings.weather.apply_city_button", "Apply City"); - - WeatherCoordinateSettingsExpander.Header = L("settings.weather.coordinates_header", "Coordinates"); - WeatherCoordinateSettingsExpander.Description = L( - "settings.weather.coordinates_desc", - "Set latitude/longitude and optional key/name."); - WeatherLatitudeNumberBox.Header = L("settings.weather.latitude_label", "Latitude"); - WeatherLongitudeNumberBox.Header = L("settings.weather.longitude_label", "Longitude"); - WeatherLocationKeyTextBox.Watermark = L("settings.weather.location_key_placeholder", "Location key (optional)"); - WeatherLocationNameTextBox.Watermark = L("settings.weather.location_name_placeholder", "Display name (optional)"); - WeatherApplyCoordinatesButton.Content = L("settings.weather.apply_coordinates_button", "Apply Coordinates"); - - WeatherAlertFilterSettingsExpander.Header = L("settings.weather.alert_filter_header", "Excluded Alerts"); - WeatherAlertFilterSettingsExpander.Description = L( - "settings.weather.alert_filter_desc", - "Alerts containing these words will not be shown. One rule per line."); - WeatherAlertListTitleTextBlock.Text = L("settings.weather.alert_list_label", "Exclude List"); - WeatherAlertListDescriptionTextBlock.Text = L("settings.weather.alert_list_desc", "One exclusion rule per line."); - WeatherExcludedAlertsTextBox.Watermark = L("settings.weather.alert_filter_placeholder", "One keyword per line"); - - WeatherIconPackSettingsExpander.Header = L("settings.weather.icon_style_header", "Weather Icon Style"); - WeatherIconPackSettingsExpander.Description = L( - "settings.weather.icon_style_desc", - "Choose Fluent Icon style for weather symbols."); - WeatherIconPackFluentRegularItem.Content = L("settings.weather.icon_style_fluent_regular", "Fluent Regular"); - WeatherIconPackFluentFilledItem.Content = L("settings.weather.icon_style_fluent_filled", "Fluent Filled"); - - WeatherNoTlsSettingsExpander.Header = L("settings.weather.no_tls_header", "No TLS Weather Request"); - WeatherNoTlsSettingsExpander.Description = L( - "settings.weather.no_tls_desc", - "Not recommended. Enable only for incompatible network environments."); - WeatherNoTlsToggleSwitch.Content = L("settings.weather.no_tls_toggle", "Allow non-TLS request fallback"); - WeatherFooterHintTextBlock.Text = L( - "settings.weather.footer_hint", - "Desktop weather widgets will reuse the location and alert exclusion settings configured here."); - - if (string.IsNullOrWhiteSpace(_weatherSearchKeyword)) - { - WeatherSearchStatusTextBlock.Text = L( - "settings.weather.search_hint", - "Search by city name and apply one location."); - } - - if (!_isWeatherPreviewInProgress) - { - WeatherPreviewResultTextBlock.Text = L( - "settings.weather.preview_hint", - "Use test fetch to verify your weather configuration."); - } - - UpdateWeatherLocationStatusText(); - - RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region"); - LanguageSettingsExpander.Header = L("settings.region.language_header", "Language"); - LanguageSettingsExpander.Description = L("settings.region.language_desc", "Select application language. Changes apply immediately."); - LanguageChineseItem.Content = L("settings.region.language_zh", "Chinese"); - LanguageEnglishItem.Content = L("settings.region.language_en", "English"); - TimeZoneSettingsExpander.Header = L("settings.region.timezone_header", "Time Zone"); - TimeZoneSettingsExpander.Description = L( - "settings.region.timezone_desc", - "Select a time zone. Clock and calendar widgets will follow this zone."); - - ApplyUpdateLocalization(); - - LauncherSettingsPanelTitleTextBlock.Text = L("settings.launcher.title", "App Launcher"); - LauncherHiddenItemsSettingsExpander.Header = L("settings.launcher.hidden_header", "Hidden Items"); - LauncherHiddenItemsSettingsExpander.Description = L( - "settings.launcher.hidden_desc", - "Review hidden launcher entries and show them again."); - LauncherHiddenItemsDescriptionTextBlock.Text = L( - "settings.launcher.hidden_hint", - "Right-click an icon in launcher to hide it. Hidden entries appear here."); - LauncherHiddenItemsEmptyTextBlock.Text = L("settings.launcher.hidden_empty", "No hidden items."); - - ApplyPluginSettingsLocalization(); - ApplyPluginMarketSettingsLocalization(); - - SettingsNavAboutItem.Content = L("settings.nav.about", "About"); - AboutPanelTitleTextBlock.Text = L("settings.about.title", "About"); - VersionTextBlock.Text = Lf( - "settings.about.version_format", - "Version: {0}", - GetAppVersionText()); - CodeNameTextBlock.Text = Lf( - "settings.about.codename_format", - "Code Name: {0}", - AppCodeName); - FontInfoTextBlock.Text = Lf( - "settings.about.font_format", - "Font: {0}", - AppFontName); - AboutStartupSettingsExpander.Header = L("settings.about.startup_header", "Windows Startup"); - AboutStartupSettingsExpander.Description = L( - "settings.about.startup_desc", - "Launch the app automatically when signing in to Windows."); - AboutRenderModeSettingsExpander.Header = L("settings.about.render_mode_header", "Rendering Mode"); - AboutRenderModeSettingsExpander.Description = L( - "settings.about.render_mode_desc", - "Choose the rendering backend. Restart the app after changing this option. Unsupported modes fall back to software."); - SetAppRenderModeComboItemContent(AppRenderingModeHelper.Default, L("settings.about.render_mode.default", "Default")); - SetAppRenderModeComboItemContent(AppRenderingModeHelper.Software, L("settings.about.render_mode.software", "Software")); - SetAppRenderModeComboItemContent(AppRenderingModeHelper.AngleEgl, L("settings.about.render_mode.angle_egl", "angleEgl")); - SetAppRenderModeComboItemContent(AppRenderingModeHelper.Wgl, L("settings.about.render_mode.wgl", "WGL")); - SetAppRenderModeComboItemContent(AppRenderingModeHelper.Vulkan, L("settings.about.render_mode.vulkan", "Vulkan")); - UpdateCurrentRenderBackendStatus(); - UpdatePendingRestartDock(); - - if (WallpaperPlacementComboBox?.ItemCount >= 5) - { - if (WallpaperPlacementComboBox.Items[0] is ComboBoxItem fillItem) fillItem.Content = L("placement.fill", "Fill"); - if (WallpaperPlacementComboBox.Items[1] is ComboBoxItem fitItem) fitItem.Content = L("placement.fit", "Fit"); - if (WallpaperPlacementComboBox.Items[2] is ComboBoxItem stretchItem) stretchItem.Content = L("placement.stretch", "Stretch"); - if (WallpaperPlacementComboBox.Items[3] is ComboBoxItem centerItem) centerItem.Content = L("placement.center", "Center"); - if (WallpaperPlacementComboBox.Items[4] is ComboBoxItem tileItem) tileItem.Content = L("placement.tile", "Tile"); - } - - - GridInfoTextBlock.Text = Lf( - "settings.grid.info_format", - "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", - DesktopGrid.ColumnDefinitions.Count, - DesktopGrid.RowDefinitions.Count, - DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d); - - InitializeTimeZoneSettings(); - BuildComponentLibraryCategoryPages(); - RenderLauncherRootTiles(); - RenderLauncherHiddenItemsList(); - UpdateOpenSettingsActionVisualState(); - UpdateWallpaperDisplay(); - } - - private void SetAppRenderModeComboItemContent(string tag, string content) - { - var item = AppRenderModeComboBox.Items - .OfType() - .FirstOrDefault(candidate => - string.Equals(candidate.Tag?.ToString(), tag, StringComparison.OrdinalIgnoreCase)); - - if (item is not null) - { - item.Content = content; - } - } - - private string GetLocalizedTimeZoneDisplayName(TimeZoneInfo timeZone) - { - var offset = timeZone.GetUtcOffset(DateTime.UtcNow); - var sign = offset >= TimeSpan.Zero ? "+" : "-"; - var hours = Math.Abs(offset.Hours); - var minutes = Math.Abs(offset.Minutes); - var name = string.IsNullOrWhiteSpace(timeZone.StandardName) - ? timeZone.DisplayName - : timeZone.StandardName; - - if (string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) && - ZhTimeZoneNames.TryGetValue(timeZone.Id, out var localizedName)) - { - name = localizedName; - } - - if (string.IsNullOrWhiteSpace(name)) - { - name = timeZone.Id; - } - - return $"(UTC{sign}{hours:D2}:{minutes:D2}) {name}"; - } - - private static string GetAppVersionText() - { - var version = typeof(MainWindow).Assembly.GetName().Version; - if (version is null || version.Major < 0 || version.Minor < 0 || version.Build < 0) - { - return FallbackAppVersion; - } - - return $"{version.Major}.{version.Minor}.{version.Build}"; - } - - private void OnLanguageSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressLanguageSelectionEvents || LanguageComboBox?.SelectedItem is not ComboBoxItem item) - { - return; - } - - var selectedLanguage = item.Tag as string; - _languageCode = _localizationService.NormalizeLanguageCode(selectedLanguage); - ApplyLocalization(); - ThemeColorStatusTextBlock.Text = Lf( - "settings.region.applied_format", - "Language switched to: {0}", - GetLanguageDisplayName(_languageCode)); - PersistSettings(); - } - - private void UpdateWeatherLocationStatusText() - { - if (WeatherLocationStatusTextBlock is null) - { - return; - } - - var modeText = _weatherLocationMode == WeatherLocationMode.Coordinates - ? L("settings.weather.mode_coordinates", "Coordinates") - : L("settings.weather.mode_city_search", "City Search"); - - if (_weatherLocationMode == WeatherLocationMode.CitySearch) - { - if (string.IsNullOrWhiteSpace(_weatherLocationKey)) - { - WeatherLocationStatusTextBlock.Text = L( - "settings.weather.status_city_empty", - "No city location is configured."); - UpdateWeatherLocationSummaryCard(); - return; - } - - var locationName = string.IsNullOrWhiteSpace(_weatherLocationName) - ? _weatherLocationKey - : _weatherLocationName; - WeatherLocationStatusTextBlock.Text = Lf( - "settings.weather.status_city_format", - "Mode: {0} | {1} | Key: {2}", - modeText, - locationName, - _weatherLocationKey); - UpdateWeatherLocationSummaryCard(); - return; - } - - WeatherLocationStatusTextBlock.Text = Lf( - "settings.weather.status_coordinates_format", - "Mode: {0} | Lat {1:F4}, Lon {2:F4} | Key: {3}", - modeText, - _weatherLatitude, - _weatherLongitude, - string.IsNullOrWhiteSpace(_weatherLocationKey) - ? BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude) - : _weatherLocationKey); - UpdateWeatherLocationSummaryCard(); - } -} - diff --git a/LanMountainDesktop/Views/MainWindow.RenderBackend.cs b/LanMountainDesktop/Views/MainWindow.RenderBackend.cs index 0c07c9e..aef13b9 100644 --- a/LanMountainDesktop/Views/MainWindow.RenderBackend.cs +++ b/LanMountainDesktop/Views/MainWindow.RenderBackend.cs @@ -7,6 +7,13 @@ public partial class MainWindow { private void UpdateCurrentRenderBackendStatus() { + if (CurrentRenderBackendLabelTextBlock is null || + CurrentRenderBackendValueTextBlock is null || + CurrentRenderBackendImplementationTextBlock is null) + { + return; + } + var backendInfo = AppRenderBackendDiagnostics.Detect(); var localizedBackend = GetLocalizedRenderBackendName(backendInfo.ActualBackend); diff --git a/LanMountainDesktop/Views/MainWindow.RestartPrompt.cs b/LanMountainDesktop/Views/MainWindow.RestartPrompt.cs deleted file mode 100644 index ed4982d..0000000 --- a/LanMountainDesktop/Views/MainWindow.RestartPrompt.cs +++ /dev/null @@ -1,115 +0,0 @@ -using System.Threading.Tasks; -using Avalonia.Interactivity; -using Avalonia.Threading; -using FluentAvalonia.UI.Controls; -using LanMountainDesktop.PluginSdk; -using LanMountainDesktop.Services; - -namespace LanMountainDesktop.Views; - -public partial class MainWindow -{ - private bool _isRestartPromptVisible; - - private void OnPendingRestartStateChanged() - { - if (Dispatcher.UIThread.CheckAccess()) - { - UpdatePendingRestartDock(); - return; - } - - Dispatcher.UIThread.Post(UpdatePendingRestartDock); - } - - private void UpdatePendingRestartDock() - { - PendingRestartDock.IsVisible = PendingRestartStateService.HasPendingRestart; - PendingRestartDockTitleTextBlock.Text = L("settings.restart_dock.title", "Restart required"); - PendingRestartDockDescriptionTextBlock.Text = L( - "settings.restart_dock.description", - "Some changes will take effect after restarting the app."); - PendingRestartDockButtonTextBlock.Text = L("settings.restart_dock.button", "Restart app"); - } - - private async void OnPendingRestartDockButtonClick(object? sender, RoutedEventArgs e) - { - await ShowGenericRestartPromptAsync(); - } - - private Task ShowRenderModeRestartPromptAsync(string selectedMode) - { - var message = Lf( - "settings.restart_dialog.render_mode_message", - "Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?", - GetLocalizedAppRenderModeDisplayName(_runningAppRenderMode), - GetLocalizedAppRenderModeDisplayName(selectedMode)); - - return ShowRestartPromptCoreAsync(message); - } - - private Task ShowGenericRestartPromptAsync() - { - return ShowRestartPromptCoreAsync(L( - "settings.restart_dock.description", - "Some changes will take effect after restarting the app.")); - } - - private async Task ShowRestartPromptCoreAsync(string message) - { - if (_isRestartPromptVisible) - { - return; - } - - _isRestartPromptVisible = true; - - try - { - var dialog = new ContentDialog - { - Title = L("settings.restart_dialog.title", "Restart required"), - Content = message, - PrimaryButtonText = L("settings.restart_dialog.restart", "Restart now"), - CloseButtonText = L("settings.restart_dialog.cancel", "Cancel"), - DefaultButton = ContentDialogButton.Primary - }; - - var result = await dialog.ShowAsync(this); - if (result == ContentDialogResult.Primary) - { - if (App.CurrentHostApplicationLifecycle?.TryRestart(new HostApplicationLifecycleRequest( - Source: nameof(MainWindow), - Reason: "User confirmed a pending restart prompt.")) != true) - { - UpdatePendingRestartDock(); - } - - return; - } - - UpdatePendingRestartDock(); - } - finally - { - _isRestartPromptVisible = false; - } - } - - private string GetLocalizedAppRenderModeDisplayName(string renderMode) - { - if (renderMode == AppRenderBackendDiagnostics.Unknown) - { - return L("settings.about.render_mode.unknown", "Unknown"); - } - - return AppRenderingModeHelper.Normalize(renderMode) switch - { - AppRenderingModeHelper.Software => L("settings.about.render_mode.software", "Software"), - AppRenderingModeHelper.AngleEgl => L("settings.about.render_mode.angle_egl", "angleEgl"), - AppRenderingModeHelper.Wgl => L("settings.about.render_mode.wgl", "WGL"), - AppRenderingModeHelper.Vulkan => L("settings.about.render_mode.vulkan", "Vulkan"), - _ => L("settings.about.render_mode.default", "Default") - }; - } -} diff --git a/LanMountainDesktop/Views/MainWindow.Settings.cs b/LanMountainDesktop/Views/MainWindow.Settings.cs deleted file mode 100644 index e813780..0000000 --- a/LanMountainDesktop/Views/MainWindow.Settings.cs +++ /dev/null @@ -1,2881 +0,0 @@ -using System; -using FluentIcons.Avalonia; -using FluentIcons.Avalonia.Fluent; -using FluentIcons.Common; -using LanMountainDesktop.Views.Components; - -using System.Collections.Generic; -using System.Globalization; -using System.IO; -using System.Linq; -using System.Runtime.InteropServices; -using System.Threading; -using System.Threading.Tasks; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Interactivity; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Platform; -using Avalonia.Platform.Storage; -using Avalonia.Styling; -using Avalonia.Threading; -using LanMountainDesktop.ComponentSystem; -using LanMountainDesktop.Models; -using LanMountainDesktop.Services; -using LanMountainDesktop.Theme; -using LibVLCSharp.Shared; - -namespace LanMountainDesktop.Views; - -public partial class MainWindow -{ - private void OnExternalAppSettingsSaved(string sourceInstanceId) - { - if (string.Equals(sourceInstanceId, _appSettingsService.InstanceId, StringComparison.Ordinal)) - { - return; - } - - ScheduleReloadFromExternalSettings(); - } - - private void OnExternalLauncherSettingsSaved(string sourceInstanceId) - { - if (string.Equals(sourceInstanceId, _launcherSettingsService.InstanceId, StringComparison.Ordinal)) - { - return; - } - - ScheduleReloadFromExternalSettings(); - } - - private void ScheduleReloadFromExternalSettings() - { - if (_externalSettingsReloadPending) - { - return; - } - - _externalSettingsReloadPending = true; - Dispatcher.UIThread.Post(() => - { - _externalSettingsReloadPending = false; - - if (!IsVisible) - { - return; - } - - ReloadFromPersistedSettings(); - }, DispatcherPriority.Background); - } - - private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) - { - if (_isComponentLibraryOpen) - { - CloseComponentLibraryWindow(reopenSettings: false); - } - - if (_isSettingsOpen) - { - CloseSettingsPage(immediate: true); - } - - AppLogger.Info("SettingsFacade", "Open settings entry is disabled during hard-cut settings API migration."); - } - - private void OnCloseSettingsClick(object? sender, RoutedEventArgs e) - { - CloseSettingsPage(); - } - - private void OnSettingsNavSelectionChanged(object? sender, FluentAvalonia.UI.Controls.NavigationViewSelectionChangedEventArgs e) - { - UpdateSettingsTabContent(); - PersistSettings(); - } - - private int GetSettingsTabIndex() - { - return ResolveSelectedSettingsTabIndex(); - } - - private void UpdateSettingsTabContent() - { - if (SettingsNavView is null || - GridSettingsPanel is null || - WallpaperSettingsPanel is null || - ColorSettingsPanel is null || - StatusBarSettingsPanel is null || - WeatherSettingsPanel is null || - RegionSettingsPanel is null || - UpdateSettingsPanel is null || - LauncherSettingsPanel is null || - AboutSettingsPanel is null || - PluginSettingsPanel is null || - PluginMarketSettingsPanel is null) - { - return; - } - - var selectedItem = SettingsNavView.SelectedItem as FluentAvalonia.UI.Controls.NavigationViewItem; - var tag = selectedItem?.Tag?.ToString(); - - WallpaperSettingsPanel.IsVisible = tag == "Wallpaper"; - GridSettingsPanel.IsVisible = tag == "Grid"; - ColorSettingsPanel.IsVisible = tag == "Color"; - StatusBarSettingsPanel.IsVisible = tag == "StatusBar"; - WeatherSettingsPanel.IsVisible = tag == "Weather"; - RegionSettingsPanel.IsVisible = tag == "Region"; - UpdateSettingsPanel.IsVisible = tag == "Update"; - AboutSettingsPanel.IsVisible = tag == "About"; - LauncherSettingsPanel.IsVisible = tag == "Launcher"; - PluginSettingsPanel.IsVisible = tag == "Plugins"; - PluginMarketSettingsPanel.IsVisible = tag == "PluginMarket"; - UpdatePluginSettingsPageVisibility(tag); - - if (tag == "Launcher") - { - RenderLauncherHiddenItemsList(); - } - - if (tag == "Plugins") - { - PluginSettingsPanel.RefreshFromRuntime(); - } - - if (tag == "PluginMarket") - { - PluginMarketSettingsPanel.RefreshFromRuntime(); - } - - if (tag == "Grid") - { - UpdateGridPreviewLayout(); - } - - ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); - UpdateVideoWallpaperPreviewVisibility(); - } - - private void OnNightModeChecked(object? sender, RoutedEventArgs e) - { - if (_suppressThemeToggleEvents) - { - return; - } - - ApplyNightModeState(true, refreshPalettes: true); - } - - private void OnNightModeUnchecked(object? sender, RoutedEventArgs e) - { - if (_suppressThemeToggleEvents) - { - return; - } - - ApplyNightModeState(false, refreshPalettes: true); - } - - private void OnRecommendedColorClick(object? sender, RoutedEventArgs e) - { - ApplyThemeColorFromButton(sender as Button, L("common.recommended", "Recommended")); - } - - private void OnMonetColorClick(object? sender, RoutedEventArgs e) - { - ApplyThemeColorFromButton(sender as Button, L("common.monet", "Monet")); - } - - private void OnRefreshMonetColorsClick(object? sender, RoutedEventArgs e) - { - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = L("settings.color.monet_refreshed", "Monet colors refreshed."); - UpdateAdaptiveTextSystem(); - PersistSettings(); - } - - private async void OnPickWallpaperClick(object? sender, RoutedEventArgs e) - { - if (StorageProvider is null) - { - _wallpaperStatus = L("settings.wallpaper.storage_unavailable", "Storage provider is unavailable."); - UpdateWallpaperDisplay(); - return; - } - - var options = new FilePickerOpenOptions - { - Title = L("filepicker.title", "Select wallpaper"), - AllowMultiple = false, - FileTypeFilter = - [ - new FilePickerFileType(L("filepicker.image_files", "Image files")) - { - Patterns = ["*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif", "*.webp"] - }, - new FilePickerFileType(L("filepicker.video_files", "Video files")) - { - Patterns = ["*.mp4", "*.mkv", "*.webm", "*.avi", "*.mov", "*.m4v"] - } - ] - }; - - var files = await StorageProvider.OpenFilePickerAsync(options); - if (files.Count == 0) - { - return; - } - - var file = files[0]; - try - { - var importedPath = await ImportWallpaperAssetAsync(file); - if (string.IsNullOrWhiteSpace(importedPath)) - { - _wallpaperStatus = L("settings.wallpaper.import_failed", "Failed to import wallpaper file."); - UpdateWallpaperDisplay(); - return; - } - - _wallpaperPath = importedPath; - var mediaType = DetectWallpaperMediaType(importedPath); - switch (mediaType) - { - case WallpaperMediaType.Image: - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = new Bitmap(importedPath); - _wallpaperVideoPath = null; - _wallpaperMediaType = WallpaperMediaType.Image; - _wallpaperStatus = L("settings.wallpaper.image_applied", "Image wallpaper applied."); - break; - case WallpaperMediaType.Video: - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = null; - _wallpaperVideoPath = importedPath; - _wallpaperMediaType = WallpaperMediaType.Video; - _wallpaperStatus = L("settings.wallpaper.video_applied", "Video wallpaper applied."); - break; - default: - _wallpaperStatus = L("settings.wallpaper.unsupported_file", "Selected file type is not supported."); - UpdateWallpaperDisplay(); - return; - } - - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = _wallpaperMediaType == WallpaperMediaType.Video - ? L("settings.color.theme_updated_video", "Video wallpaper updated. Theme colors refreshed.") - : L("settings.color.theme_updated_wallpaper", "Wallpaper updated. Monet colors refreshed."); - PersistSettings(); - } - catch (Exception ex) - { - _wallpaperStatus = Lf("settings.wallpaper.apply_failed_format", "Failed to apply wallpaper: {0}", ex.Message); - UpdateWallpaperDisplay(); - } - } - - private void OnClearWallpaperClick(object? sender, RoutedEventArgs e) - { - StopVideoWallpaper(); - _wallpaperVideoPath = null; - _wallpaperMediaType = WallpaperMediaType.None; - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = null; - _wallpaperPath = null; - _wallpaperStatus = L("settings.wallpaper.cleared", "Background reset to solid color."); - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = L("settings.color.theme_cleared_wallpaper", "Wallpaper cleared. Monet colors refreshed."); - PersistSettings(); - } - - private void OnWallpaperPlacementSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - ApplyWallpaperBrush(); - if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null) - { - _wallpaperStatus = Lf( - "settings.wallpaper.mode_format", - "Wallpaper mode: {0}.", - GetLocalizedPlacementDisplayName(GetSelectedWallpaperPlacement())); - } - else if (_wallpaperMediaType == WallpaperMediaType.Video) - { - _wallpaperStatus = L("settings.wallpaper.video_mode", "Video wallpaper mode uses automatic fill mode."); - } - - UpdateWallpaperDisplay(); - PersistSettings(); - } - - private void ApplyWallpaperBrush() - { - if (_wallpaperMediaType == WallpaperMediaType.Video && - !string.IsNullOrWhiteSpace(_wallpaperVideoPath)) - { - DesktopWallpaperLayer.Background = Brushes.Transparent; - WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); - PlayVideoWallpaper(_wallpaperVideoPath); - return; - } - - StopVideoWallpaper(); - if (_wallpaperBitmap is null) - { - var fallbackBackground = GetThemeDefaultDesktopBackground(); - DesktopWallpaperLayer.Background = fallbackBackground; - WallpaperPreviewViewport.Background = fallbackBackground; - return; - } - - var placement = GetSelectedWallpaperPlacement(); - DesktopWallpaperLayer.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false); - WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true); - UpdateVideoWallpaperPreviewVisibility(); - } - - private void UpdateWallpaperDisplay() - { - if (WallpaperPathTextBlock is null || - WallpaperStatusTextBlock is null || - WallpaperPreviewViewport is null || - WallpaperPlacementComboBox is null) - { - return; - } - - WallpaperPathTextBlock.Text = string.IsNullOrWhiteSpace(_wallpaperPath) - ? L("settings.wallpaper.no_selection", "No wallpaper selected.") - : Path.GetFileName(_wallpaperPath); - WallpaperStatusTextBlock.Text = _wallpaperStatus; - WallpaperPlacementComboBox.IsEnabled = _wallpaperMediaType != WallpaperMediaType.Video; - - if (_wallpaperMediaType == WallpaperMediaType.Video) - { - WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); - return; - } - - if (_wallpaperBitmap is null) - { - WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground(); - return; - } - - WallpaperPreviewViewport.Background = CreateWallpaperBrush( - _wallpaperBitmap, - GetSelectedWallpaperPlacement(), - true); - } - - private ImageBrush CreateWallpaperBrush(Bitmap bitmap, WallpaperPlacement placement, bool forPreview) - { - var brush = new ImageBrush - { - Source = bitmap, - Stretch = Stretch.UniformToFill, - AlignmentX = AlignmentX.Center, - AlignmentY = AlignmentY.Center, - TileMode = TileMode.None - }; - - if (forPreview) - { - // For preview, we want to simulate how the image looks on a real screen. - // Assuming a nominal screen width of 1920 for calculation. - // The preview width is 480, so the scale is 480/1920 = 0.25. - const double nominalScreenWidth = 1920.0; - const double previewWidth = 480.0; - double scale = previewWidth / nominalScreenWidth; - - if (placement == WallpaperPlacement.Center) - { - brush.Transform = new ScaleTransform(scale, scale); - } - } - - switch (placement) - { - case WallpaperPlacement.Fill: - brush.Stretch = Stretch.UniformToFill; - break; - case WallpaperPlacement.Fit: - brush.Stretch = Stretch.Uniform; - break; - case WallpaperPlacement.Stretch: - brush.Stretch = Stretch.Fill; - break; - case WallpaperPlacement.Center: - brush.Stretch = Stretch.None; - break; - case WallpaperPlacement.Tile: - brush.Stretch = Stretch.None; - brush.TileMode = TileMode.Tile; - var tileSize = forPreview ? 96d : 220d; - brush.DestinationRect = new RelativeRect(0, 0, tileSize, tileSize, RelativeUnit.Absolute); - break; - } - - return brush; - } - - private WallpaperPlacement GetSelectedWallpaperPlacement() - { - return WallpaperPlacementComboBox?.SelectedIndex switch - { - 1 => WallpaperPlacement.Fit, - 2 => WallpaperPlacement.Stretch, - 3 => WallpaperPlacement.Center, - 4 => WallpaperPlacement.Tile, - _ => WallpaperPlacement.Fill - }; - } - - private static string GetPlacementDisplayName(WallpaperPlacement placement) - { - return placement switch - { - WallpaperPlacement.Fill => "Fill", - WallpaperPlacement.Fit => "Fit", - WallpaperPlacement.Stretch => "Stretch", - WallpaperPlacement.Center => "Center", - WallpaperPlacement.Tile => "Tile", - _ => "Fill" - }; - } - - private IBrush GetThemeDefaultDesktopBackground() - { - if (Resources.TryGetResource("AdaptiveSurfaceBaseBrush", ActualThemeVariant, out var resource) && - resource is IBrush themedBrush) - { - return themedBrush; - } - - return _defaultDesktopBackground ?? - (_isNightMode - ? new SolidColorBrush(Color.Parse("#FF0B1220")) - : new SolidColorBrush(Color.Parse("#FFF3F7FB"))); - } - - private static int GetPlacementIndexFromSetting(string? placement) - { - if (string.IsNullOrWhiteSpace(placement)) - { - return 0; - } - - return placement.Trim().ToLowerInvariant() switch - { - "fit" => 1, - "stretch" => 2, - "center" => 3, - "tile" => 4, - _ => 0 - }; - } - - private void TryRestoreWallpaper(string? savedWallpaperPath) - { - StopVideoWallpaper(); - _wallpaperMediaType = WallpaperMediaType.None; - _wallpaperVideoPath = null; - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = null; - _wallpaperPath = null; - - if (string.IsNullOrWhiteSpace(savedWallpaperPath)) - { - _wallpaperStatus = L("settings.wallpaper.default_status", "Current background uses solid color."); - return; - } - - if (!Path.IsPathRooted(savedWallpaperPath) || !File.Exists(savedWallpaperPath)) - { - _wallpaperStatus = L( - "settings.wallpaper.saved_not_found", - "Saved wallpaper file was not found. Using solid color background."); - return; - } - - try - { - var mediaType = DetectWallpaperMediaType(savedWallpaperPath); - switch (mediaType) - { - case WallpaperMediaType.Image: - _wallpaperBitmap = new Bitmap(savedWallpaperPath); - _wallpaperPath = savedWallpaperPath; - _wallpaperMediaType = WallpaperMediaType.Image; - _wallpaperStatus = L("settings.wallpaper.restored", "Wallpaper restored from saved settings."); - break; - case WallpaperMediaType.Video: - _wallpaperVideoPath = savedWallpaperPath; - _wallpaperPath = savedWallpaperPath; - _wallpaperMediaType = WallpaperMediaType.Video; - _wallpaperStatus = L("settings.wallpaper.video_restored", "Video wallpaper restored from saved settings."); - break; - default: - _wallpaperStatus = L( - "settings.wallpaper.unsupported_file", - "Saved wallpaper type is not supported. Using solid color background."); - break; - } - } - catch - { - _wallpaperStatus = L( - "settings.wallpaper.restore_failed", - "Failed to restore saved wallpaper. Using solid color background."); - _wallpaperBitmap?.Dispose(); - _wallpaperBitmap = null; - _wallpaperMediaType = WallpaperMediaType.None; - _wallpaperVideoPath = null; - _wallpaperPath = null; - } - } - - private static bool TryParseColor(string? colorText, out Color color) - { - color = default; - if (string.IsNullOrWhiteSpace(colorText)) - { - return false; - } - - try - { - color = Color.Parse(colorText); - return true; - } - catch - { - return false; - } - } - - private static WallpaperMediaType DetectWallpaperMediaType(string path) - { - var extension = Path.GetExtension(path); - if (string.IsNullOrWhiteSpace(extension)) - { - return WallpaperMediaType.None; - } - - if (SupportedImageExtensions.Contains(extension)) - { - return WallpaperMediaType.Image; - } - - if (SupportedVideoExtensions.Contains(extension)) - { - return WallpaperMediaType.Video; - } - - return WallpaperMediaType.None; - } - - private static async Task ImportWallpaperAssetAsync(IStorageFile file) - { - try - { - var extension = Path.GetExtension(file.Name); - if (string.IsNullOrWhiteSpace(extension)) - { - extension = ".bin"; - } - - var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); - var wallpaperDirectory = Path.Combine(appData, "LanMountainDesktop", "Wallpapers"); - Directory.CreateDirectory(wallpaperDirectory); - - var destinationPath = Path.Combine( - wallpaperDirectory, - $"{DateTime.Now:yyyyMMdd_HHmmss}_{Guid.NewGuid():N}{extension}"); - - await using var sourceStream = await file.OpenReadAsync(); - await using var destinationStream = File.Create(destinationPath); - await sourceStream.CopyToAsync(destinationStream); - return destinationPath; - } - catch - { - return null; - } - } - - private void EnsureVideoWallpaperPlayers() - { - Core.Initialize(); - _libVlc ??= new LibVLC("--quiet"); - - if (_videoWallpaperPlayer is null) - { - _videoWallpaperPlayer = new MediaPlayer(_libVlc) - { - EnableHardwareDecoding = false - }; - } - } - - private void EnsureDesktopVideoFrameRefreshTimer() - { - if (_desktopVideoFrameRefreshTimer is not null) - { - return; - } - - _desktopVideoFrameRefreshTimer = new DispatcherTimer - { - Interval = TimeSpan.FromMilliseconds(33) - }; - _desktopVideoFrameRefreshTimer.Tick += OnDesktopVideoFrameRefreshTimerTick; - } - - private void StartDesktopVideoFrameRefreshTimer() - { - EnsureDesktopVideoFrameRefreshTimer(); - if (_desktopVideoFrameRefreshTimer?.IsEnabled == false) - { - _desktopVideoFrameRefreshTimer.Start(); - } - } - - private void StopDesktopVideoFrameRefreshTimer() - { - if (_desktopVideoFrameRefreshTimer?.IsEnabled == true) - { - _desktopVideoFrameRefreshTimer.Stop(); - } - } - - private void OnDesktopVideoFrameRefreshTimerTick(object? sender, EventArgs e) - { - PushDesktopVideoFrameToWallpaperImage(); - } - - private void UpdateVideoWallpaperPreviewVisibility() - { - var shouldShowPreview = - _wallpaperMediaType == WallpaperMediaType.Video && - _isSettingsOpen && - SettingsPage.IsVisible && - WallpaperSettingsPanel.IsVisible && - _wallpaperPreviewSnapshotBitmap is not null; - - WallpaperPreviewVideoImage.IsVisible = shouldShowPreview; - if (shouldShowPreview && !ReferenceEquals(WallpaperPreviewVideoImage.Source, _wallpaperPreviewSnapshotBitmap)) - { - WallpaperPreviewVideoImage.Source = _wallpaperPreviewSnapshotBitmap; - } - } - - private void InvalidateVideoWallpaperPreviewSnapshot() - { - _wallpaperPreviewSnapshotPending = true; - _wallpaperPreviewSnapshotBitmap?.Dispose(); - _wallpaperPreviewSnapshotBitmap = null; - WallpaperPreviewVideoImage.Source = null; - } - - private void CaptureVideoWallpaperPreviewSnapshotFromStagingBuffer() - { - if (!_wallpaperPreviewSnapshotPending || - _desktopVideoStagingBuffer is null || - _desktopVideoFrameWidth <= 0 || - _desktopVideoFrameHeight <= 0 || - _desktopVideoFramePitch <= 0) - { - return; - } - - _wallpaperPreviewSnapshotBitmap?.Dispose(); - _wallpaperPreviewSnapshotBitmap = new WriteableBitmap( - new PixelSize(_desktopVideoFrameWidth, _desktopVideoFrameHeight), - new Vector(96, 96), - PixelFormat.Bgra8888, - AlphaFormat.Opaque); - - using var framebuffer = _wallpaperPreviewSnapshotBitmap.Lock(); - var rows = Math.Min(framebuffer.Size.Height, _desktopVideoFrameHeight); - var bytesPerRow = Math.Min(framebuffer.RowBytes, _desktopVideoFramePitch); - for (var row = 0; row < rows; row++) - { - var sourceOffset = row * _desktopVideoFramePitch; - var destinationPtr = IntPtr.Add(framebuffer.Address, row * framebuffer.RowBytes); - Marshal.Copy(_desktopVideoStagingBuffer, sourceOffset, destinationPtr, bytesPerRow); - } - - _wallpaperPreviewSnapshotPending = false; - WallpaperPreviewVideoImage.Source = _wallpaperPreviewSnapshotBitmap; - UpdateVideoWallpaperPreviewVisibility(); - } - - private bool ConfigureDesktopVideoRenderer() - { - if (_videoWallpaperPlayer is null || DesktopVideoWallpaperImage is null) - { - return false; - } - - var (targetWidth, targetHeight) = GetDesktopVideoRenderSize(); - var targetPitch = targetWidth * 4; - var targetBufferSize = targetPitch * targetHeight; - if (targetBufferSize <= 0) - { - return false; - } - - if (targetWidth == _desktopVideoFrameWidth && - targetHeight == _desktopVideoFrameHeight && - _desktopVideoFrameBufferPtr != IntPtr.Zero && - _desktopVideoBitmap is not null) - { - return true; - } - - ReleaseDesktopVideoRendererResources(); - - try - { - _desktopVideoFrameWidth = targetWidth; - _desktopVideoFrameHeight = targetHeight; - _desktopVideoFramePitch = targetPitch; - _desktopVideoFrameBufferSize = targetBufferSize; - _desktopVideoFrameBufferPtr = Marshal.AllocHGlobal(_desktopVideoFrameBufferSize); - _desktopVideoStagingBuffer = new byte[_desktopVideoFrameBufferSize]; - _desktopVideoBitmap = new WriteableBitmap( - new PixelSize(_desktopVideoFrameWidth, _desktopVideoFrameHeight), - new Vector(96, 96), - PixelFormat.Bgra8888, - AlphaFormat.Opaque); - EnsureDesktopVideoCallbacks(); - _videoWallpaperPlayer.SetVideoCallbacks( - _desktopVideoLockCallback!, - _desktopVideoUnlockCallback!, - _desktopVideoDisplayCallback!); - _videoWallpaperPlayer.SetVideoFormat( - "RV32", - (uint)_desktopVideoFrameWidth, - (uint)_desktopVideoFrameHeight, - (uint)_desktopVideoFramePitch); - DesktopVideoWallpaperImage.Source = _desktopVideoBitmap; - InvalidateVideoWallpaperPreviewSnapshot(); - return true; - } - catch - { - ReleaseDesktopVideoRendererResources(); - return false; - } - } - - private (int Width, int Height) GetDesktopVideoRenderSize() - { - var hostWidth = DesktopHost?.Bounds.Width ?? Bounds.Width; - var hostHeight = DesktopHost?.Bounds.Height ?? Bounds.Height; - var pixelWidth = Math.Max(1, (int)Math.Round(hostWidth * RenderScaling)); - var pixelHeight = Math.Max(1, (int)Math.Round(hostHeight * RenderScaling)); - - const int maxPixelCount = 1920 * 1080; - var pixelCount = (long)pixelWidth * pixelHeight; - if (pixelCount > maxPixelCount) - { - var scale = Math.Sqrt((double)maxPixelCount / pixelCount); - pixelWidth = Math.Max(1, (int)Math.Round(pixelWidth * scale)); - pixelHeight = Math.Max(1, (int)Math.Round(pixelHeight * scale)); - } - - return (pixelWidth, pixelHeight); - } - - private void EnsureDesktopVideoCallbacks() - { - _desktopVideoLockCallback ??= OnDesktopVideoFrameLock; - _desktopVideoUnlockCallback ??= OnDesktopVideoFrameUnlock; - _desktopVideoDisplayCallback ??= OnDesktopVideoFrameDisplay; - } - - private IntPtr OnDesktopVideoFrameLock(IntPtr opaque, IntPtr planes) - { - Monitor.Enter(_desktopVideoFrameSync); - if (_desktopVideoFrameBufferPtr == IntPtr.Zero) - { - Marshal.WriteIntPtr(planes, IntPtr.Zero); - Monitor.Exit(_desktopVideoFrameSync); - return IntPtr.Zero; - } - - Marshal.WriteIntPtr(planes, _desktopVideoFrameBufferPtr); - return IntPtr.Zero; - } - - private void OnDesktopVideoFrameUnlock(IntPtr opaque, IntPtr picture, IntPtr planes) - { - if (Monitor.IsEntered(_desktopVideoFrameSync)) - { - Monitor.Exit(_desktopVideoFrameSync); - } - } - - private void OnDesktopVideoFrameDisplay(IntPtr opaque, IntPtr picture) - { - Interlocked.Exchange(ref _desktopVideoFrameDirtyFlag, 1); - } - - private void PushDesktopVideoFrameToWallpaperImage() - { - if (Interlocked.Exchange(ref _desktopVideoFrameDirtyFlag, 0) == 0) - { - return; - } - - if (_desktopVideoBitmap is null || - _desktopVideoStagingBuffer is null || - _desktopVideoFrameBufferPtr == IntPtr.Zero || - _desktopVideoFrameBufferSize <= 0) - { - return; - } - - lock (_desktopVideoFrameSync) - { - if (_desktopVideoFrameBufferPtr == IntPtr.Zero) - { - return; - } - - Marshal.Copy(_desktopVideoFrameBufferPtr, _desktopVideoStagingBuffer, 0, _desktopVideoFrameBufferSize); - } - - using var framebuffer = _desktopVideoBitmap.Lock(); - var rows = Math.Min(framebuffer.Size.Height, _desktopVideoFrameHeight); - var bytesPerRow = Math.Min(framebuffer.RowBytes, _desktopVideoFramePitch); - for (var row = 0; row < rows; row++) - { - var sourceOffset = row * _desktopVideoFramePitch; - var destinationPtr = IntPtr.Add(framebuffer.Address, row * framebuffer.RowBytes); - Marshal.Copy(_desktopVideoStagingBuffer, sourceOffset, destinationPtr, bytesPerRow); - } - - if (DesktopVideoWallpaperImage is not null && - !ReferenceEquals(DesktopVideoWallpaperImage.Source, _desktopVideoBitmap)) - { - DesktopVideoWallpaperImage.Source = _desktopVideoBitmap; - } - - CaptureVideoWallpaperPreviewSnapshotFromStagingBuffer(); - } - - private void ReleaseDesktopVideoRendererResources() - { - Interlocked.Exchange(ref _desktopVideoFrameDirtyFlag, 0); - - if (DesktopVideoWallpaperImage is not null) - { - DesktopVideoWallpaperImage.Source = null; - } - - InvalidateVideoWallpaperPreviewSnapshot(); - WallpaperPreviewVideoImage.Source = null; - - _desktopVideoBitmap?.Dispose(); - _desktopVideoBitmap = null; - _desktopVideoStagingBuffer = null; - _desktopVideoFrameWidth = 0; - _desktopVideoFrameHeight = 0; - _desktopVideoFramePitch = 0; - _desktopVideoFrameBufferSize = 0; - - lock (_desktopVideoFrameSync) - { - if (_desktopVideoFrameBufferPtr != IntPtr.Zero) - { - Marshal.FreeHGlobal(_desktopVideoFrameBufferPtr); - _desktopVideoFrameBufferPtr = IntPtr.Zero; - } - } - } - - private void PlayVideoWallpaper(string videoPath) - { - if (!File.Exists(videoPath)) - { - _wallpaperStatus = L("settings.wallpaper.video_not_found", "Video wallpaper file not found."); - StopVideoWallpaper(); - return; - } - - try - { - EnsureVideoWallpaperPlayers(); - if (_videoWallpaperPlayer is null || - _libVlc is null || - DesktopVideoWallpaperImage is null) - { - _wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable."); - StopVideoWallpaper(); - return; - } - - if (!ConfigureDesktopVideoRenderer()) - { - _wallpaperStatus = L("settings.wallpaper.video_player_unavailable", "Video player is unavailable."); - StopVideoWallpaper(); - return; - } - - _videoWallpaperMedia?.Dispose(); - _videoWallpaperMedia = new Media(_libVlc, new Uri(videoPath)); - _videoWallpaperMedia.AddOption(":input-repeat=65535"); - InvalidateVideoWallpaperPreviewSnapshot(); - _videoWallpaperPlayer.Play(_videoWallpaperMedia); - StartDesktopVideoFrameRefreshTimer(); - DesktopVideoWallpaperImage.IsVisible = true; - UpdateVideoWallpaperPreviewVisibility(); - } - catch (Exception ex) - { - _wallpaperStatus = Lf("settings.wallpaper.video_play_failed_format", "Failed to play video wallpaper: {0}", ex.Message); - StopVideoWallpaper(); - } - } - - private void StopVideoWallpaper() - { - if (DesktopVideoWallpaperImage is not null) - { - DesktopVideoWallpaperImage.IsVisible = false; - } - - WallpaperPreviewVideoImage.IsVisible = false; - - if (_videoWallpaperPlayer is not null) - { - _videoWallpaperPlayer.Stop(); - } - - StopDesktopVideoFrameRefreshTimer(); - ReleaseDesktopVideoRendererResources(); - _videoWallpaperMedia?.Dispose(); - _videoWallpaperMedia = null; - } - - private void PersistSettings() - { - if (_suppressSettingsPersistence) - { - return; - } - - _appSettingsService.Save(BuildAppSettingsSnapshot()); - _desktopLayoutSettingsService.Save(BuildDesktopLayoutSettingsSnapshot()); - _launcherSettingsService.Save(BuildLauncherSettingsSnapshot()); - - } - - internal void ReloadFromPersistedSettings() - { - _suppressSettingsPersistence = true; - try - { - var snapshot = _appSettingsService.Load(); - var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load(); - var launcherSnapshot = _launcherSettingsService.Load(); - - if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId)) - { - _timeZoneService.SetTimeZoneById(snapshot.TimeZoneId); - } - - _targetShortSideCells = Math.Clamp( - snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(), - MinShortSideCells, - MaxShortSideCells); - - _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); - _suppressGridSpacingEvents = true; - GridSpacingPresetComboBox.SelectedIndex = - string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0; - _suppressGridSpacingEvents = false; - - _desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent); - _suppressGridInsetEvents = true; - GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent; - GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent; - _suppressGridInsetEvents = false; - - _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode); - _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30); - _suppressStatusBarSpacingEvents = true; - StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch - { - "Compact" => 0, - "Custom" => 2, - _ => 1 - }; - StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent; - StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent; - StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase); - _suppressStatusBarSpacingEvents = false; - - GridSizeNumberBox.Value = _targetShortSideCells; - GridSizeSlider.Value = _targetShortSideCells; - - RestoreSettingsTabSelection(snapshot); - - UpdateSettingsTabContent(); - WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement); - _defaultDesktopBackground = DesktopWallpaperLayer.Background; - ApplyTaskbarSettings(snapshot); - InitializeLocalization(snapshot.LanguageCode); - InitializeWeatherSettings(snapshot); - _ = _componentSettingsService.Load(); - InitializeAutoStartWithWindowsSetting(snapshot); - InitializeAppRenderModeSetting(snapshot); - InitializeUpdateSettings(snapshot); - InitializeDesktopSurfaceState(desktopLayoutSnapshot); - InitializeLauncherVisibilitySettings(launcherSnapshot); - InitializeDesktopComponentPlacements(desktopLayoutSnapshot); - InitializeSettingsIcons(); - - TryRestoreWallpaper(snapshot.WallpaperPath); - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - - if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) - { - _selectedThemeColor = savedThemeColor; - } - - _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); - ApplyNightModeState(_isNightMode, refreshPalettes: true); - _suppressStatusBarToggleEvents = true; - StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); - _suppressStatusBarToggleEvents = false; - ApplyLocalization(); - ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor); - RebuildDesktopGrid(); - LoadLauncherEntriesAsync(); - InitializeTimeZoneSettings(); - ClockWidget.SetTimeZoneService(_timeZoneService); - UpdateWallpaperPreviewLayout(); - UpdateGridPreviewLayout(); - UpdateAdaptiveTextSystem(); - ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); - } - finally - { - _suppressSettingsPersistence = false; - } - } - - private AppSettingsSnapshot BuildAppSettingsSnapshot() - { - var snapshot = _appSettingsService.Load(); - snapshot.GridShortSideCells = _targetShortSideCells; - snapshot.GridSpacingPreset = _gridSpacingPreset; - snapshot.DesktopEdgeInsetPercent = _desktopEdgeInsetPercent; - snapshot.IsNightMode = _isNightMode; - snapshot.ThemeColor = _selectedThemeColor.ToString(); - snapshot.WallpaperPath = _wallpaperPath; - snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement()); - snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex()); - snapshot.SettingsTabTag = GetSelectedSettingsTabTag(); - snapshot.LanguageCode = _languageCode; - snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id; - snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode); - snapshot.WeatherLocationKey = _weatherLocationKey; - snapshot.WeatherLocationName = _weatherLocationName; - snapshot.WeatherLatitude = _weatherLatitude; - snapshot.WeatherLongitude = _weatherLongitude; - snapshot.WeatherAutoRefreshLocation = _weatherAutoRefreshLocation; - snapshot.WeatherLocationQuery = BuildLegacyWeatherLocationQuery(); - snapshot.WeatherExcludedAlerts = _weatherExcludedAlertsRaw; - snapshot.WeatherIconPackId = _weatherIconPackId; - snapshot.WeatherNoTlsRequests = _weatherNoTlsRequests; - snapshot.AutoStartWithWindows = _autoStartWithWindows; - snapshot.AppRenderMode = _selectedAppRenderMode; - snapshot.AutoCheckUpdates = _autoCheckUpdates; - snapshot.IncludePrereleaseUpdates = IncludePrereleaseUpdates; - snapshot.UpdateChannel = IncludePrereleaseUpdates ? UpdateChannelPreview : UpdateChannelStable; - snapshot.TopStatusComponentIds = _topStatusComponentIds.ToList(); - snapshot.PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(); - snapshot.EnableDynamicTaskbarActions = _enableDynamicTaskbarActions; - snapshot.TaskbarLayoutMode = _taskbarLayoutMode; - snapshot.ClockDisplayFormat = _clockDisplayFormat == ClockDisplayFormat.HourMinute ? "HourMinute" : "HourMinuteSecond"; - snapshot.StatusBarSpacingMode = _statusBarSpacingMode; - snapshot.StatusBarCustomSpacingPercent = _statusBarCustomSpacingPercent; - return snapshot; - } - private DesktopLayoutSettingsSnapshot BuildDesktopLayoutSettingsSnapshot() - { - return new DesktopLayoutSettingsSnapshot - { - DesktopPageCount = _desktopPageCount, - CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex, - DesktopComponentPlacements = _desktopComponentPlacements.ToList() - }; - } - private LauncherSettingsSnapshot BuildLauncherSettingsSnapshot() - { - return new LauncherSettingsSnapshot - { - HiddenLauncherFolderPaths = _hiddenLauncherFolderPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList(), - HiddenLauncherAppPaths = _hiddenLauncherAppPaths.OrderBy(path => path, StringComparer.OrdinalIgnoreCase).ToList() - }; - } - - private IDisposable? _persistSettingsDebounceTimer; - - private void SchedulePersistSettings(int delayMs = 200) - { - if (_suppressSettingsPersistence) - { - return; - } - - _persistSettingsDebounceTimer?.Dispose(); - _persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() => - { - _persistSettingsDebounceTimer = null; - PersistSettings(); - }, TimeSpan.FromMilliseconds(Math.Max(0, delayMs))); - } - - private void InitializeWeatherSettings(AppSettingsSnapshot snapshot) - { - _suppressWeatherLocationEvents = true; - try - { - _weatherLocationMode = ParseWeatherLocationMode(snapshot.WeatherLocationMode); - _weatherLocationKey = snapshot.WeatherLocationKey?.Trim() ?? string.Empty; - _weatherLocationName = snapshot.WeatherLocationName?.Trim() ?? string.Empty; - _weatherLatitude = NormalizeLatitude(snapshot.WeatherLatitude); - _weatherLongitude = NormalizeLongitude(snapshot.WeatherLongitude); - _weatherAutoRefreshLocation = snapshot.WeatherAutoRefreshLocation; - _weatherExcludedAlertsRaw = snapshot.WeatherExcludedAlerts?.Trim() ?? string.Empty; - _weatherIconPackId = string.IsNullOrWhiteSpace(snapshot.WeatherIconPackId) - ? "FluentRegular" - : snapshot.WeatherIconPackId.Trim(); - _weatherNoTlsRequests = snapshot.WeatherNoTlsRequests; - _weatherSearchKeyword = string.Empty; - - var legacyQuery = snapshot.WeatherLocationQuery?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(_weatherLocationKey) && !string.IsNullOrWhiteSpace(legacyQuery)) - { - _weatherLocationKey = legacyQuery; - } - - if (string.IsNullOrWhiteSpace(_weatherLocationName) && !string.IsNullOrWhiteSpace(legacyQuery)) - { - _weatherLocationName = legacyQuery; - } - - SelectWeatherLocationModeInUi(_weatherLocationMode); - if (WeatherAutoRefreshToggleSwitch is not null) - { - WeatherAutoRefreshToggleSwitch.IsChecked = _weatherAutoRefreshLocation; - } - - if (WeatherNoTlsToggleSwitch is not null) - { - WeatherNoTlsToggleSwitch.IsChecked = _weatherNoTlsRequests; - } - - if (WeatherCitySearchTextBox is not null) - { - WeatherCitySearchTextBox.Text = string.Empty; - } - - if (WeatherCityResultsComboBox is not null) - { - WeatherCityResultsComboBox.Items.Clear(); - } - - if (WeatherLocationKeyTextBox is not null) - { - WeatherLocationKeyTextBox.Text = _weatherLocationKey; - } - - if (WeatherLocationNameTextBox is not null) - { - WeatherLocationNameTextBox.Text = _weatherLocationName; - } - - if (WeatherLatitudeNumberBox is not null) - { - WeatherLatitudeNumberBox.Value = _weatherLatitude; - } - - if (WeatherLongitudeNumberBox is not null) - { - WeatherLongitudeNumberBox.Value = _weatherLongitude; - } - - if (WeatherExcludedAlertsTextBox is not null) - { - WeatherExcludedAlertsTextBox.Text = _weatherExcludedAlertsRaw; - } - - SelectWeatherIconPackInUi(_weatherIconPackId); - - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = L( - "settings.weather.search_hint", - "Search by city name and apply one location."); - } - - if (WeatherCoordinateStatusTextBlock is not null) - { - WeatherCoordinateStatusTextBlock.Text = string.Empty; - } - - if (WeatherPreviewResultTextBlock is not null) - { - WeatherPreviewResultTextBlock.Text = L( - "settings.weather.preview_hint", - "Use test fetch to verify your weather configuration."); - } - - UpdateWeatherPreviewSummary( - weatherCode: null, - temperatureText: "--", - updatedAt: null); - } - finally - { - _suppressWeatherLocationEvents = false; - } - - UpdateWeatherLocationModePanels(); - UpdateWeatherLocationStatusText(); - } - - private void InitializeAutoStartWithWindowsSetting(AppSettingsSnapshot snapshot) - { - _autoStartWithWindows = OperatingSystem.IsWindows() - ? _windowsStartupService.IsEnabled() - : snapshot.AutoStartWithWindows; - - if (AutoStartWithWindowsToggleSwitch is null) - { - return; - } - - _suppressAutoStartToggleEvents = true; - try - { - AutoStartWithWindowsToggleSwitch.IsEnabled = OperatingSystem.IsWindows(); - AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows; - } - finally - { - _suppressAutoStartToggleEvents = false; - } - } - - private void InitializeAppRenderModeSetting(AppSettingsSnapshot snapshot) - { - _selectedAppRenderMode = AppRenderingModeHelper.Normalize(snapshot.AppRenderMode); - _runningAppRenderMode = ResolveActiveAppRenderModeForUi(_selectedAppRenderMode); - var renderModeForUi = PendingRestartStateService.HasPendingReason(PendingRestartStateService.RenderModeReason) - ? _selectedAppRenderMode - : _runningAppRenderMode; - - if (AppRenderModeComboBox is null) - { - return; - } - - _suppressAppRenderModeSelectionEvents = true; - try - { - AppRenderModeComboBox.IsEnabled = OperatingSystem.IsWindows(); - SelectAppRenderModeInUi(renderModeForUi); - } - finally - { - _suppressAppRenderModeSelectionEvents = false; - } - } - - private void SelectAppRenderModeInUi(string renderMode) - { - if (AppRenderModeComboBox is null) - { - return; - } - - AppRenderModeComboBox.SelectedIndex = GetAppRenderModeComboBoxIndex(renderMode); - } - - private static int GetAppRenderModeComboBoxIndex(string renderMode) - { - return AppRenderingModeHelper.Normalize(renderMode) switch - { - AppRenderingModeHelper.Software => 1, - AppRenderingModeHelper.AngleEgl => 2, - AppRenderingModeHelper.Wgl => 3, - AppRenderingModeHelper.Vulkan => 4, - _ => 0 - }; - } - - private static string ResolveActiveAppRenderModeForUi(string configuredRenderMode) - { - var detectedRenderMode = AppRenderBackendDiagnostics.Detect().ActualBackend; - return string.Equals(detectedRenderMode, AppRenderBackendDiagnostics.Unknown, StringComparison.Ordinal) - ? configuredRenderMode - : AppRenderingModeHelper.Normalize(detectedRenderMode); - } - - private static WeatherLocationMode ParseWeatherLocationMode(string? value) - { - return string.Equals(value, "Coordinates", StringComparison.OrdinalIgnoreCase) - ? WeatherLocationMode.Coordinates - : WeatherLocationMode.CitySearch; - } - - private static string ToWeatherLocationModeTag(WeatherLocationMode mode) - { - return mode == WeatherLocationMode.Coordinates ? "Coordinates" : "CitySearch"; - } - - private static double NormalizeLatitude(double value) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - { - return 39.9042; - } - - return Math.Clamp(value, -90, 90); - } - - private static double NormalizeLongitude(double value) - { - if (double.IsNaN(value) || double.IsInfinity(value)) - { - return 116.4074; - } - - return Math.Clamp(value, -180, 180); - } - - private string BuildLegacyWeatherLocationQuery() - { - if (!string.IsNullOrWhiteSpace(_weatherLocationName)) - { - return _weatherLocationName; - } - - if (!string.IsNullOrWhiteSpace(_weatherLocationKey)) - { - return _weatherLocationKey; - } - - return string.Create( - CultureInfo.InvariantCulture, - $"{_weatherLatitude:F4},{_weatherLongitude:F4}"); - } - - private void SelectWeatherLocationModeInUi(WeatherLocationMode mode) - { - var targetTag = ToWeatherLocationModeTag(mode); - var selected = false; - if (WeatherLocationModeComboBox is not null) - { - foreach (var item in WeatherLocationModeComboBox.Items.OfType()) - { - if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase)) - { - WeatherLocationModeComboBox.SelectedItem = item; - selected = true; - break; - } - } - - if (!selected) - { - WeatherLocationModeComboBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0; - } - } - - if (WeatherLocationModeChipListBox is null) - { - return; - } - - foreach (var item in WeatherLocationModeChipListBox.Items.OfType()) - { - if (string.Equals(item.Tag?.ToString(), targetTag, StringComparison.OrdinalIgnoreCase)) - { - WeatherLocationModeChipListBox.SelectedItem = item; - return; - } - } - - WeatherLocationModeChipListBox.SelectedIndex = mode == WeatherLocationMode.Coordinates ? 1 : 0; - } - - private void SelectWeatherIconPackInUi(string iconPackId) - { - if (WeatherIconPackComboBox is null) - { - return; - } - - foreach (var item in WeatherIconPackComboBox.Items.OfType()) - { - if (string.Equals(item.Tag?.ToString(), iconPackId, StringComparison.OrdinalIgnoreCase)) - { - WeatherIconPackComboBox.SelectedItem = item; - return; - } - } - - WeatherIconPackComboBox.SelectedIndex = 0; - _weatherIconPackId = "FluentRegular"; - } - - private void UpdateWeatherLocationModePanels() - { - if (WeatherCitySearchSettingsExpander is not null) - { - WeatherCitySearchSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.CitySearch; - } - - if (WeatherCoordinateSettingsExpander is not null) - { - WeatherCoordinateSettingsExpander.IsVisible = _weatherLocationMode == WeatherLocationMode.Coordinates; - } - - UpdateWeatherLocationSummaryCard(); - } - - private void OnWeatherLocationModeSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressWeatherLocationEvents || WeatherLocationModeComboBox?.SelectedItem is not ComboBoxItem item) - { - return; - } - - _weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString()); - _suppressWeatherLocationEvents = true; - try - { - SelectWeatherLocationModeInUi(_weatherLocationMode); - } - finally - { - _suppressWeatherLocationEvents = false; - } - UpdateWeatherLocationModePanels(); - UpdateWeatherLocationStatusText(); - PersistSettings(); - } - - private void OnWeatherLocationModeChipSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressWeatherLocationEvents || WeatherLocationModeChipListBox?.SelectedItem is not ListBoxItem item) - { - return; - } - - _weatherLocationMode = ParseWeatherLocationMode(item.Tag?.ToString()); - _suppressWeatherLocationEvents = true; - try - { - SelectWeatherLocationModeInUi(_weatherLocationMode); - } - finally - { - _suppressWeatherLocationEvents = false; - } - - UpdateWeatherLocationModePanels(); - UpdateWeatherLocationStatusText(); - PersistSettings(); - } - - private void OnWeatherAutoRefreshToggled(object? sender, RoutedEventArgs e) - { - if (_suppressWeatherLocationEvents || WeatherAutoRefreshToggleSwitch is null) - { - return; - } - - _weatherAutoRefreshLocation = WeatherAutoRefreshToggleSwitch.IsChecked == true; - PersistSettings(); - } - - private void OnWeatherExcludedAlertsLostFocus(object? sender, RoutedEventArgs e) - { - if (WeatherExcludedAlertsTextBox is null) - { - return; - } - - _weatherExcludedAlertsRaw = WeatherExcludedAlertsTextBox.Text?.Trim() ?? string.Empty; - PersistSettings(); - } - - private void OnWeatherIconPackSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressWeatherLocationEvents || WeatherIconPackComboBox?.SelectedItem is not ComboBoxItem item) - { - return; - } - - _weatherIconPackId = item.Tag?.ToString() switch - { - "FluentFilled" => "FluentFilled", - _ => "FluentRegular" - }; - - if (WeatherPreviewIconSymbol is not null) - { - WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase) - ? IconVariant.Filled - : IconVariant.Regular; - } - - PersistSettings(); - } - - private void OnWeatherNoTlsToggled(object? sender, RoutedEventArgs e) - { - if (_suppressWeatherLocationEvents || WeatherNoTlsToggleSwitch is null) - { - return; - } - - _weatherNoTlsRequests = WeatherNoTlsToggleSwitch.IsChecked == true; - PersistSettings(); - } - - private void OnAutoStartWithWindowsToggled(object? sender, RoutedEventArgs e) - { - if (_suppressAutoStartToggleEvents || AutoStartWithWindowsToggleSwitch is null) - { - return; - } - - var requested = AutoStartWithWindowsToggleSwitch.IsChecked == true; - if (!OperatingSystem.IsWindows()) - { - _autoStartWithWindows = false; - _suppressAutoStartToggleEvents = true; - try - { - AutoStartWithWindowsToggleSwitch.IsEnabled = false; - AutoStartWithWindowsToggleSwitch.IsChecked = false; - } - finally - { - _suppressAutoStartToggleEvents = false; - } - - PersistSettings(); - return; - } - - var applied = _windowsStartupService.SetEnabled(requested); - _autoStartWithWindows = _windowsStartupService.IsEnabled(); - - if (!applied || _autoStartWithWindows != requested) - { - _suppressAutoStartToggleEvents = true; - try - { - AutoStartWithWindowsToggleSwitch.IsChecked = _autoStartWithWindows; - } - finally - { - _suppressAutoStartToggleEvents = false; - } - } - - PersistSettings(); - } - - private void OnAppRenderModeSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressAppRenderModeSelectionEvents || AppRenderModeComboBox is null) - { - return; - } - - var selectedMode = AppRenderingModeHelper.Normalize( - TryGetSelectedComboBoxTag(AppRenderModeComboBox)); - - if (string.Equals(_selectedAppRenderMode, selectedMode, StringComparison.Ordinal)) - { - return; - } - - _selectedAppRenderMode = selectedMode; - PersistSettings(); - var requiresRestart = !string.Equals(_runningAppRenderMode, selectedMode, StringComparison.Ordinal); - PendingRestartStateService.SetPending(PendingRestartStateService.RenderModeReason, requiresRestart); - UpdatePendingRestartDock(); - - if (requiresRestart) - { - _ = ShowRenderModeRestartPromptAsync(selectedMode); - } - } - - private async void OnSearchWeatherCityClick(object? sender, RoutedEventArgs e) - { - if (_isWeatherSearchInProgress || WeatherCitySearchTextBox is null || WeatherCityResultsComboBox is null) - { - return; - } - - var keyword = WeatherCitySearchTextBox.Text?.Trim() ?? string.Empty; - if (string.IsNullOrWhiteSpace(keyword)) - { - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = L( - "settings.weather.search_required", - "Please enter a city keyword first."); - } - - return; - } - - _weatherSearchKeyword = keyword; - _isWeatherSearchInProgress = true; - SetWeatherSearchBusy(isBusy: true); - try - { - var result = await _weatherDataService.SearchLocationsAsync(keyword, ResolveWeatherApiLocale()); - if (!result.Success || result.Data is null) - { - WeatherCityResultsComboBox.Items.Clear(); - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = Lf( - "settings.weather.search_failed_format", - "Search failed: {0}", - result.ErrorMessage ?? result.ErrorCode ?? "Unknown error"); - } - - return; - } - - var locations = result.Data - .Where(location => !string.IsNullOrWhiteSpace(location.LocationKey)) - .Take(80) - .ToList(); - - WeatherCityResultsComboBox.Items.Clear(); - foreach (var location in locations) - { - WeatherCityResultsComboBox.Items.Add(new ComboBoxItem - { - Content = FormatWeatherLocationDisplayName(location), - Tag = location - }); - } - - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = locations.Count == 0 - ? L("settings.weather.search_no_results", "No locations were found.") - : Lf( - "settings.weather.search_result_count_format", - "Found {0} locations.", - locations.Count); - } - - if (locations.Count > 0) - { - WeatherCityResultsComboBox.SelectedIndex = 0; - } - } - catch (Exception ex) - { - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = Lf( - "settings.weather.search_failed_format", - "Search failed: {0}", - ex.Message); - } - } - finally - { - _isWeatherSearchInProgress = false; - SetWeatherSearchBusy(isBusy: false); - } - } - - private static string FormatWeatherLocationDisplayName(WeatherLocation location) - { - var affiliation = string.IsNullOrWhiteSpace(location.Affiliation) - ? string.Empty - : $" ({location.Affiliation})"; - return string.Create( - CultureInfo.InvariantCulture, - $"{location.Name}{affiliation} | {location.LocationKey}"); - } - - private static string BuildWeatherLocationName(WeatherLocation location) - { - if (string.IsNullOrWhiteSpace(location.Affiliation)) - { - return location.Name; - } - - return string.Create( - CultureInfo.InvariantCulture, - $"{location.Name} ({location.Affiliation})"); - } - - private void OnApplyWeatherCitySelectionClick(object? sender, RoutedEventArgs e) - { - if (WeatherCityResultsComboBox?.SelectedItem is not ComboBoxItem item || - item.Tag is not WeatherLocation location) - { - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = L( - "settings.weather.search_select_required", - "Please select one location from search results."); - } - - return; - } - - _weatherLocationMode = WeatherLocationMode.CitySearch; - _weatherLocationKey = location.LocationKey.Trim(); - _weatherLocationName = BuildWeatherLocationName(location); - _weatherLatitude = NormalizeLatitude(location.Latitude); - _weatherLongitude = NormalizeLongitude(location.Longitude); - - _suppressWeatherLocationEvents = true; - try - { - SelectWeatherLocationModeInUi(_weatherLocationMode); - if (WeatherLocationKeyTextBox is not null) - { - WeatherLocationKeyTextBox.Text = _weatherLocationKey; - } - - if (WeatherLocationNameTextBox is not null) - { - WeatherLocationNameTextBox.Text = _weatherLocationName; - } - - if (WeatherLatitudeNumberBox is not null) - { - WeatherLatitudeNumberBox.Value = _weatherLatitude; - } - - if (WeatherLongitudeNumberBox is not null) - { - WeatherLongitudeNumberBox.Value = _weatherLongitude; - } - } - finally - { - _suppressWeatherLocationEvents = false; - } - - if (WeatherSearchStatusTextBlock is not null) - { - WeatherSearchStatusTextBlock.Text = Lf( - "settings.weather.search_applied_format", - "Location applied: {0}", - _weatherLocationName); - } - - UpdateWeatherLocationModePanels(); - UpdateWeatherLocationStatusText(); - PersistSettings(); - } - - private void OnApplyWeatherCoordinatesClick(object? sender, RoutedEventArgs e) - { - if (WeatherLatitudeNumberBox is null || WeatherLongitudeNumberBox is null) - { - return; - } - - var latitude = NormalizeLatitude(WeatherLatitudeNumberBox.Value); - var longitude = NormalizeLongitude(WeatherLongitudeNumberBox.Value); - var keyInput = WeatherLocationKeyTextBox?.Text?.Trim() ?? string.Empty; - var nameInput = WeatherLocationNameTextBox?.Text?.Trim() ?? string.Empty; - - _weatherLocationMode = WeatherLocationMode.Coordinates; - _weatherLatitude = latitude; - _weatherLongitude = longitude; - _weatherLocationKey = string.IsNullOrWhiteSpace(keyInput) - ? BuildCoordinateLocationKey(latitude, longitude) - : keyInput; - _weatherLocationName = string.IsNullOrWhiteSpace(nameInput) - ? Lf( - "settings.weather.coordinates_default_name_format", - "Coordinate {0:F4}, {1:F4}", - latitude, - longitude) - : nameInput; - - _suppressWeatherLocationEvents = true; - try - { - SelectWeatherLocationModeInUi(_weatherLocationMode); - if (WeatherLocationKeyTextBox is not null && string.IsNullOrWhiteSpace(keyInput)) - { - WeatherLocationKeyTextBox.Text = _weatherLocationKey; - } - - if (WeatherLocationNameTextBox is not null && string.IsNullOrWhiteSpace(nameInput)) - { - WeatherLocationNameTextBox.Text = _weatherLocationName; - } - } - finally - { - _suppressWeatherLocationEvents = false; - } - - if (WeatherCoordinateStatusTextBlock is not null) - { - WeatherCoordinateStatusTextBlock.Text = Lf( - "settings.weather.coordinates_saved_format", - "Coordinates saved: {0:F4}, {1:F4}", - _weatherLatitude, - _weatherLongitude); - } - - UpdateWeatherLocationModePanels(); - UpdateWeatherLocationStatusText(); - PersistSettings(); - } - - private static string BuildCoordinateLocationKey(double latitude, double longitude) - { - return string.Create( - CultureInfo.InvariantCulture, - $"coord:{latitude:F4},{longitude:F4}"); - } - - private async void OnTestWeatherRequestClick(object? sender, RoutedEventArgs e) - { - if (_isWeatherPreviewInProgress) - { - return; - } - - if (string.IsNullOrWhiteSpace(_weatherLocationKey)) - { - if (_weatherLocationMode == WeatherLocationMode.Coordinates) - { - _weatherLocationKey = BuildCoordinateLocationKey(_weatherLatitude, _weatherLongitude); - } - else - { - if (WeatherPreviewResultTextBlock is not null) - { - WeatherPreviewResultTextBlock.Text = L( - "settings.weather.preview_missing_location", - "Please apply one weather location before testing."); - } - - UpdateWeatherPreviewSummary( - weatherCode: null, - temperatureText: "--", - updatedAt: null); - - return; - } - } - - _isWeatherPreviewInProgress = true; - SetWeatherPreviewBusy(isBusy: true); - try - { - var query = new WeatherQuery( - LocationKey: _weatherLocationKey, - Latitude: _weatherLatitude, - Longitude: _weatherLongitude, - ForecastDays: 3, - Locale: ResolveWeatherApiLocale(), - IsGlobal: false, - ForceRefresh: true); - - var result = await _weatherDataService.GetWeatherAsync(query); - if (!result.Success || result.Data is null) - { - if (WeatherPreviewResultTextBlock is not null) - { - WeatherPreviewResultTextBlock.Text = Lf( - "settings.weather.preview_failed_format", - "Test fetch failed: {0}", - result.ErrorMessage ?? result.ErrorCode ?? "Unknown error"); - } - - UpdateWeatherPreviewSummary( - weatherCode: null, - temperatureText: "--", - updatedAt: DateTimeOffset.Now); - - return; - } - - var snapshot = result.Data; - var location = string.IsNullOrWhiteSpace(snapshot.LocationName) - ? (!string.IsNullOrWhiteSpace(_weatherLocationName) ? _weatherLocationName : _weatherLocationKey) - : snapshot.LocationName; - var weather = snapshot.Current.WeatherText ?? - L("settings.weather.preview_unknown", "Unknown"); - var temperature = snapshot.Current.TemperatureC.HasValue - ? FormatWeatherPreviewTemperature(snapshot.Current.TemperatureC.Value) - : "--"; - var updatedAt = snapshot.ObservationTime ?? snapshot.FetchedAt; - - if (WeatherPreviewResultTextBlock is not null) - { - WeatherPreviewResultTextBlock.Text = Lf( - "settings.weather.preview_success_format", - "Test success: {0} | {1} | {2}", - location, - weather, - temperature); - } - - UpdateWeatherPreviewSummary( - weatherCode: snapshot.Current.WeatherCode, - temperatureText: temperature, - updatedAt: updatedAt); - } - catch (Exception ex) - { - if (WeatherPreviewResultTextBlock is not null) - { - WeatherPreviewResultTextBlock.Text = Lf( - "settings.weather.preview_failed_format", - "Test fetch failed: {0}", - ex.Message); - } - - UpdateWeatherPreviewSummary( - weatherCode: null, - temperatureText: "--", - updatedAt: DateTimeOffset.Now); - } - finally - { - _isWeatherPreviewInProgress = false; - SetWeatherPreviewBusy(isBusy: false); - } - } - - private void UpdateWeatherPreviewSummary(int? weatherCode, string temperatureText, DateTimeOffset? updatedAt) - { - if (WeatherPreviewIconImage is not null) - { - var kind = HyperOS3WeatherTheme.ResolveVisualKind(weatherCode, _isNightMode); - WeatherPreviewIconImage.Source = HyperOS3WeatherAssetLoader.LoadImage( - HyperOS3WeatherTheme.ResolveIconAsset(kind)) ?? - HyperOS3WeatherAssetLoader.LoadImage(HyperOS3WeatherTheme.ResolveHeroIconAsset(kind)); - } - - if (WeatherPreviewIconSymbol is not null) - { - WeatherPreviewIconSymbol.Symbol = ResolveWeatherPreviewSymbol(weatherCode, _isNightMode); - WeatherPreviewIconSymbol.IconVariant = string.Equals(_weatherIconPackId, "FluentFilled", StringComparison.OrdinalIgnoreCase) - ? IconVariant.Filled - : IconVariant.Regular; - } - - if (WeatherPreviewTemperatureTextBlock is not null) - { - WeatherPreviewTemperatureTextBlock.Text = string.IsNullOrWhiteSpace(temperatureText) ? "--" : temperatureText; - } - - if (WeatherPreviewUpdatedTextBlock is null) - { - return; - } - - WeatherPreviewUpdatedTextBlock.Text = updatedAt.HasValue - ? updatedAt.Value.LocalDateTime.ToString("yyyy/M/d HH:mm:ss", CultureInfo.InvariantCulture) - : "-"; - } - - private static string FormatWeatherPreviewTemperature(double temperatureC) - { - return string.Create(CultureInfo.InvariantCulture, $"{temperatureC:0.#}°C"); - } - - private static Symbol ResolveWeatherPreviewSymbol(int? weatherCode, bool isNight) - { - return weatherCode switch - { - 0 => isNight ? Symbol.WeatherMoon : Symbol.WeatherSunny, - 1 or 2 => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay, - 3 or 7 => Symbol.WeatherRainShowersDay, - 8 or 9 => Symbol.WeatherRain, - 4 => Symbol.WeatherThunderstorm, - 13 or 14 or 15 or 16 => Symbol.WeatherSnow, - 18 or 32 => Symbol.WeatherFog, - _ => isNight ? Symbol.WeatherPartlyCloudyNight : Symbol.WeatherPartlyCloudyDay - }; - } - - private void UpdateWeatherLocationSummaryCard() - { - if (WeatherLocationSelectionTitleTextBlock is null || - WeatherLocationSelectionDescriptionTextBlock is null || - WeatherLocationValueTextBlock is null) - { - return; - } - - if (_weatherLocationMode == WeatherLocationMode.Coordinates) - { - WeatherLocationSelectionTitleTextBlock.Text = L("settings.weather.coordinates_selection_label", "Coordinate Location"); - WeatherLocationSelectionDescriptionTextBlock.Text = L( - "settings.weather.location_coordinates_summary_desc", - "Set latitude/longitude and optional location name used for weather queries."); - WeatherLocationValueTextBlock.Text = string.IsNullOrWhiteSpace(_weatherLocationName) - ? string.Create(CultureInfo.InvariantCulture, $"{_weatherLatitude:F4}, {_weatherLongitude:F4}") - : _weatherLocationName; - return; - } - - WeatherLocationSelectionTitleTextBlock.Text = L("settings.weather.city_selection_label", "City Selection"); - WeatherLocationSelectionDescriptionTextBlock.Text = L( - "settings.weather.location_city_summary_desc", - "Select the current city used for weather queries."); - WeatherLocationValueTextBlock.Text = !string.IsNullOrWhiteSpace(_weatherLocationName) - ? _weatherLocationName - : !string.IsNullOrWhiteSpace(_weatherLocationKey) - ? _weatherLocationKey - : L("settings.weather.location_not_selected", "No location selected"); - } - - private void SetWeatherSearchBusy(bool isBusy) - { - if (WeatherSearchButton is not null) - { - WeatherSearchButton.IsEnabled = !isBusy; - } - - if (WeatherSearchProgressRing is not null) - { - WeatherSearchProgressRing.IsVisible = isBusy; - } - } - - private void SetWeatherPreviewBusy(bool isBusy) - { - if (WeatherPreviewButton is not null) - { - WeatherPreviewButton.IsEnabled = !isBusy; - } - - if (WeatherPreviewProgressRing is not null) - { - WeatherPreviewProgressRing.IsVisible = isBusy; - } - } - - private string ResolveWeatherApiLocale() - { - return string.Equals(_languageCode, "zh-CN", StringComparison.OrdinalIgnoreCase) - ? "zh_cn" - : "en_us"; - } - - private void UpdateAdaptiveTextSystem() - { - var isLightBackground = _isSettingsOpen - ? !_isNightMode - : CalculateCurrentBackgroundLuminance() >= LightBackgroundLuminanceThreshold; - var isLightNavBackground = _isSettingsOpen - ? !_isNightMode - : CalculateBrushLuminance(SettingsNavView?.Background) >= LightBackgroundLuminanceThreshold; - var context = new ThemeColorContext( - _selectedThemeColor, - isLightBackground, - isLightNavBackground, - _isNightMode); - - ThemeColorSystemService.ApplyThemeResources(Resources, context); - GlassEffectService.ApplyGlassResources(Resources, context); - if (_fluentAvaloniaTheme is not null) - { - _fluentAvaloniaTheme.CustomAccentColor = _selectedThemeColor; - } - } - - private double CalculateCurrentBackgroundLuminance() - { - if (_isSettingsOpen) - { - return CalculateBrushLuminance(SettingsContentPanel?.Background ?? SettingsPage?.Background); - } - - if (_wallpaperMediaType == WallpaperMediaType.Video) - { - return CalculateRelativeLuminance(Color.Parse("#FF0B1220")); - } - - if (_wallpaperBitmap is not null) - { - return CalculateBitmapAverageLuminance(_wallpaperBitmap); - } - - return CalculateBrushLuminance(DesktopWallpaperLayer.Background ?? _defaultDesktopBackground); - } - - private void ApplyNightModeState(bool enabled, bool refreshPalettes) - { - _isNightMode = enabled; - RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light; - UpdateThemeModeIcon(); - - _suppressThemeToggleEvents = true; - NightModeToggleSwitch.IsChecked = enabled; - _suppressThemeToggleEvents = false; - - if (refreshPalettes) - { - RefreshColorPalettes(); - EnsureSelectedThemeColor(); - } - - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = Lf( - "settings.color.mode_status_format", - "Theme mode: {0}.", - enabled ? L("common.night", "Night") : L("common.day", "Day")); - UpdateAdaptiveTextSystem(); - ApplyWallpaperBrush(); - PersistSettings(); - } - - private void RefreshColorPalettes() - { - var palette = _monetColorService.BuildPalette(_wallpaperBitmap, _isNightMode); - _recommendedColors = palette.RecommendedColors; - _monetColors = palette.MonetColors; - ApplyColorPaletteToButtons(_recommendedColors, GetRecommendedColorTargets()); - ApplyColorPaletteToButtons(_monetColors, GetMonetColorTargets()); - } - - private void ApplyColorPaletteToButtons( - IReadOnlyList colors, - IReadOnlyList<(Button Button, Border Swatch)> targets) - { - for (var i = 0; i < targets.Count; i++) - { - var color = i < colors.Count - ? colors[i] - : Color.Parse("#00000000"); - var (button, swatch) = targets[i]; - button.Tag = color.ToString(); - button.IsEnabled = i < colors.Count; - swatch.Background = i < colors.Count - ? new SolidColorBrush(color) - : new SolidColorBrush(Color.Parse("#00000000")); - } - } - - private IReadOnlyList<(Button Button, Border Swatch)> GetRecommendedColorTargets() - { - return - [ - (RecommendedColorButton1, RecommendedColorSwatch1), - (RecommendedColorButton2, RecommendedColorSwatch2), - (RecommendedColorButton3, RecommendedColorSwatch3), - (RecommendedColorButton4, RecommendedColorSwatch4), - (RecommendedColorButton5, RecommendedColorSwatch5), - (RecommendedColorButton6, RecommendedColorSwatch6) - ]; - } - - private IReadOnlyList<(Button Button, Border Swatch)> GetMonetColorTargets() - { - return - [ - (MonetColorButton1, MonetColorSwatch1), - (MonetColorButton2, MonetColorSwatch2), - (MonetColorButton3, MonetColorSwatch3), - (MonetColorButton4, MonetColorSwatch4), - (MonetColorButton5, MonetColorSwatch5), - (MonetColorButton6, MonetColorSwatch6) - ]; - } - - private void EnsureSelectedThemeColor() - { - if (ContainsColor(_recommendedColors, _selectedThemeColor) || - ContainsColor(_monetColors, _selectedThemeColor)) - { - return; - } - - if (_recommendedColors.Count > 0) - { - _selectedThemeColor = _recommendedColors[0]; - return; - } - - if (_monetColors.Count > 0) - { - _selectedThemeColor = _monetColors[0]; - } - } - - private void ApplyThemeColorFromButton(Button? button, string sourceLabel) - { - if (!TryGetButtonColor(button, out var color)) - { - return; - } - - _selectedThemeColor = color; - UpdateThemeColorSelectionState(); - ThemeColorStatusTextBlock.Text = Lf( - "settings.color.theme_applied_format", - "{0} color applied: {1}.", - sourceLabel, - _selectedThemeColor); - UpdateAdaptiveTextSystem(); - PersistSettings(); - } - - private void UpdateThemeColorSelectionState() - { - UpdateColorSelectionVisuals(GetRecommendedColorTargets()); - UpdateColorSelectionVisuals(GetMonetColorTargets()); - } - - private void UpdateColorSelectionVisuals(IReadOnlyList<(Button Button, Border Swatch)> targets) - { - foreach (var (button, swatch) in targets) - { - var isSelected = TryGetButtonColor(button, out var color) && AreSameColor(color, _selectedThemeColor); - button.Classes.Set("swatch-button", true); - button.Classes.Set("swatch-selected", isSelected); - swatch.BorderThickness = new Thickness(0); - swatch.Opacity = isSelected ? 1 : 0.9; - } - } - - private static bool TryGetButtonColor(Button? button, out Color color) - { - color = default; - if (button?.Tag is not string colorText || string.IsNullOrWhiteSpace(colorText)) - { - return false; - } - - try - { - color = Color.Parse(colorText); - return true; - } - catch - { - return false; - } - } - - - private static bool ContainsColor(IReadOnlyList colors, Color target) - { - for (var i = 0; i < colors.Count; i++) - { - if (AreSameColor(colors[i], target)) - { - return true; - } - } - - return false; - } - - private static bool AreSameColor(Color left, Color right) - { - return left.R == right.R && left.G == right.G && left.B == right.B; - } - - - private static double CalculateBrushLuminance(IBrush? brush) - { - if (brush is ISolidColorBrush solidBrush) - { - return CalculateRelativeLuminance(solidBrush.Color); - } - - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - - private static double CalculateBitmapAverageLuminance(Bitmap bitmap) - { - try - { - var sampleWidth = Math.Clamp(bitmap.PixelSize.Width, 1, 48); - var sampleHeight = Math.Clamp(bitmap.PixelSize.Height, 1, 48); - - using var scaledBitmap = bitmap.CreateScaledBitmap( - new PixelSize(sampleWidth, sampleHeight), - BitmapInterpolationMode.MediumQuality); - using var writeable = new WriteableBitmap( - scaledBitmap.PixelSize, - new Vector(96, 96), - PixelFormat.Bgra8888, - AlphaFormat.Premul); - using var framebuffer = writeable.Lock(); - - scaledBitmap.CopyPixels(framebuffer, AlphaFormat.Premul); - - var rowBytes = framebuffer.RowBytes; - var byteCount = rowBytes * framebuffer.Size.Height; - if (byteCount <= 0 || framebuffer.Address == IntPtr.Zero) - { - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - - var pixelBuffer = new byte[byteCount]; - Marshal.Copy(framebuffer.Address, pixelBuffer, 0, byteCount); - - double luminanceSum = 0; - var pixelCount = 0; - for (var y = 0; y < framebuffer.Size.Height; y++) - { - var rowOffset = y * rowBytes; - for (var x = 0; x < framebuffer.Size.Width; x++) - { - var index = rowOffset + (x * 4); - var alpha = pixelBuffer[index + 3] / 255d; - if (alpha <= 0.01) - { - continue; - } - - var blue = (pixelBuffer[index] / 255d) / alpha; - var green = (pixelBuffer[index + 1] / 255d) / alpha; - var red = (pixelBuffer[index + 2] / 255d) / alpha; - - red = Math.Clamp(red, 0, 1); - green = Math.Clamp(green, 0, 1); - blue = Math.Clamp(blue, 0, 1); - - luminanceSum += CalculateRelativeLuminance(red, green, blue); - pixelCount++; - } - } - - return pixelCount > 0 - ? luminanceSum / pixelCount - : CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - catch - { - return CalculateRelativeLuminance(Color.Parse("#FF020617")); - } - } - - private static double CalculateRelativeLuminance(Color color) - { - return CalculateRelativeLuminance(color.R / 255d, color.G / 255d, color.B / 255d); - } - - private static double CalculateRelativeLuminance(double red, double green, double blue) - { - var linearRed = ToLinearRgb(red); - var linearGreen = ToLinearRgb(green); - var linearBlue = ToLinearRgb(blue); - return (0.2126 * linearRed) + (0.7152 * linearGreen) + (0.0722 * linearBlue); - } - - private static double ToLinearRgb(double value) - { - return value <= 0.04045 - ? value / 12.92 - : Math.Pow((value + 0.055) / 1.055, 2.4); - } - - private void OpenSettingsPage() - { - if (_isComponentLibraryOpen) - { - return; - } - - if (_isSettingsOpen) - { - return; - } - - _isSettingsOpen = true; - UpdateDesktopPageAwareComponentContext(); - UpdateAdaptiveTextSystem(); - if (_wallpaperMediaType == WallpaperMediaType.Video) - { - UpdateVideoWallpaperPreviewVisibility(); - } - else - { - ApplyWallpaperBrush(); - } - ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 30; - } - SettingsPage.IsVisible = true; - SettingsPage.Opacity = 0; - UpdateVideoWallpaperPreviewVisibility(); - UpdateSettingsViewportInsets(Math.Max(1, _currentDesktopCellSize)); - - UpdateWallpaperPreviewLayout(); - - Dispatcher.UIThread.Post(() => - { - if (!_isSettingsOpen) - { - return; - } - - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 0; - } - SettingsPage.Opacity = 1; - }, DispatcherPriority.Background); - } - - private void CloseSettingsPage(bool immediate = false) - { - if (!_isSettingsOpen) - { - return; - } - - _isSettingsOpen = false; - UpdateDesktopPageAwareComponentContext(); - UpdateAdaptiveTextSystem(); - UpdateVideoWallpaperPreviewVisibility(); - if (_wallpaperMediaType != WallpaperMediaType.Video) - { - ApplyWallpaperBrush(); - } - ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); - - if (immediate) - { - SettingsPage.Opacity = 0; - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 30; - } - SettingsPage.IsVisible = false; - return; - } - - if (_settingsContentPanelTransform is not null) - { - _settingsContentPanelTransform.Y = 30; - } - SettingsPage.Opacity = 0; - - DispatcherTimer.RunOnce(() => - { - if (_isSettingsOpen) - { - return; - } - - SettingsPage.IsVisible = false; - }, TimeSpan.FromMilliseconds(SettingsTransitionDurationMs)); - } - - private void InitializeSettingsIcons() - { - const IconVariant variant = IconVariant.Regular; - - if (WallpaperPlacementSettingsExpander is not null) - { - WallpaperPlacementSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Wallpaper, - IconVariant = variant - }; - } - - if (ThemeColorSettingsExpander is not null) - { - ThemeColorSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Color, - IconVariant = variant - }; - } - - if (StatusBarClockSettingsExpander is not null) - { - StatusBarClockSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Clock, - IconVariant = variant - }; - } - - if (StatusBarSpacingSettingsExpander is not null) - { - StatusBarSpacingSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.TextLineSpacing, - IconVariant = variant - }; - } - - if (WeatherLocationSettingsExpander is not null) - { - WeatherLocationSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.WeatherSunny, - IconVariant = variant - }; - } - - if (WeatherPreviewSettingsExpander is not null) - { - WeatherPreviewSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.WeatherSunny, - IconVariant = variant - }; - } - - if (WeatherAlertFilterSettingsExpander is not null) - { - WeatherAlertFilterSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Info, - IconVariant = variant - }; - } - - if (WeatherIconPackSettingsExpander is not null) - { - WeatherIconPackSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Color, - IconVariant = variant - }; - } - - if (WeatherNoTlsSettingsExpander is not null) - { - WeatherNoTlsSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Globe, - IconVariant = variant - }; - } - - if (LanguageSettingsExpander is not null) - { - LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Translate, - IconVariant = variant - }; - } - - if (TimeZoneSettingsExpander is not null) - { - TimeZoneSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.GlobeClock, - IconVariant = variant - }; - } - - if (UpdateOptionsSettingsExpander is not null) - { - UpdateOptionsSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.ArrowClockwiseDashesSettings, - IconVariant = variant - }; - } - - if (UpdateActionsSettingsExpander is not null) - { - UpdateActionsSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.ArrowDownload, - IconVariant = variant - }; - } - - if (AboutStartupSettingsExpander is not null) - { - AboutStartupSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = Symbol.Play, - IconVariant = variant - }; - } - - UpdateThemeModeIcon(); - } - - private void UpdateThemeModeIcon() - { - if (ThemeModeSettingsExpander is null) - { - return; - } - - ThemeModeSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource - { - Symbol = _isNightMode ? Symbol.WeatherMoon : Symbol.WeatherSunny, - IconVariant = IconVariant.Regular - }; - } - - // ======================================================================== - // --- UserControl Forwarding Properties (Definitive Catch-All List) --- - // These properties allow legacy code in MainWindow.axaml.cs and other partials - // to access controls that have been moved into independent UserControls. - // ======================================================================== - - // --- WallpaperSettingsPage --- - internal TextBlock WallpaperPanelTitleTextBlock => WallpaperSettingsPanel.FindControl("WallpaperPanelTitleTextBlock")!; - internal TextBlock WallpaperPathTextBlock => WallpaperSettingsPanel.FindControl("WallpaperPathTextBlock")!; - internal TextBlock WallpaperStatusTextBlock => WallpaperSettingsPanel.FindControl("WallpaperStatusTextBlock")!; - internal ComboBox WallpaperPlacementComboBox => WallpaperSettingsPanel.FindControl("WallpaperPlacementComboBox")!; - internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewHost")!; - internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl("WallpaperPreviewFrame")!; - internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl("WallpaperPreviewViewport")!; - internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl("WallpaperPreviewVideoImage")!; - internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl("WallpaperPreviewGrid")!; - internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusBarHost")!; - internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl("WallpaperPreviewTopStatusComponentsPanel")!; - internal LanMountainDesktop.Views.Components.ClockWidget WallpaperPreviewClockWidget => WallpaperSettingsPanel.FindControl("WallpaperPreviewClockWidget")!; - internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperSettingsPanel.FindControl("WallpaperPreviewBottomTaskbarContainer")!; - internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTaskbarFixedActionsHost")!; - internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperSettingsPanel.FindControl("WallpaperPreviewBackButtonVisual")!; - internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperSettingsPanel.FindControl("WallpaperPreviewBackButtonTextBlock")!; - internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTaskbarDynamicActionsHost")!; - internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperSettingsPanel.FindControl("WallpaperPreviewTaskbarSettingsActionHost")!; - internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperSettingsPanel.FindControl("WallpaperPreviewComponentLibraryVisual")!; - internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperSettingsPanel.FindControl("WallpaperPreviewComponentLibraryTextBlock")!; - internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperSettingsPanel.FindControl("WallpaperPreviewSettingsButtonIcon")!; - internal Button PickWallpaperButton => WallpaperSettingsPanel.FindControl - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - 22 - 28 - 0.92 - 0.95 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 3c294ee..b5ab096 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -20,7 +20,9 @@ using Avalonia.Threading; using FluentAvalonia.Styling; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; +using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; +using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Theme; using LanMountainDesktop.Views.Components; using LibVLCSharp.Shared; @@ -73,21 +75,26 @@ public partial class MainWindow : Window [ TaskbarActionId.MinimizeToWindows ]; - private readonly DesktopGridLayoutService _gridLayoutService = new(); - private readonly MonetColorService _monetColorService = new(); - private readonly AppSettingsService _appSettingsService = new(); - private readonly DesktopLayoutSettingsService _desktopLayoutSettingsService = new(); - private readonly LauncherSettingsService _launcherSettingsService = new(); - private readonly ComponentSettingsService _componentSettingsService = new(); + private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); + private readonly IGridSettingsService _gridSettingsService; + private readonly IThemeAppearanceService _themeSettingsService; + private readonly IWeatherSettingsService _weatherSettingsService; + private readonly IRegionSettingsService _regionSettingsService; + private readonly IUpdateSettingsService _updateSettingsService; + private readonly ISettingsService _settingsService; + private readonly IComponentLayoutStore _componentLayoutStore = ComponentDomainStorageProvider.Instance; + private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance; + private readonly IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService(); private readonly LocalizationService _localizationService = new(); - private readonly TimeZoneService _timeZoneService = new(); + private readonly TimeZoneService _timeZoneService; private readonly WindowsStartupService _windowsStartupService = new(); - private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop"); - private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); + private readonly IWeatherInfoService _weatherDataService; private readonly IRecommendationInfoService _recommendationInfoService = new RecommendationDataService(); private readonly ICalculatorDataService _calculatorDataService = new CalculatorDataService(); private readonly ComponentRegistry _componentRegistry; private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry; + private readonly IComponentLibraryService _componentLibraryService; + private readonly IComponentLibraryWindowService _componentLibraryWindowService = new ComponentLibraryWindowService(); private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme; private readonly HashSet _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet _pinnedTaskbarActions = []; @@ -171,71 +178,25 @@ public partial class MainWindow : Window { var pluginRuntimeService = (Application.Current as App)?.PluginRuntimeService; _componentRegistry = DesktopComponentRegistryFactory.Create(pluginRuntimeService); + _settingsService = _settingsFacade.Settings; + _gridSettingsService = _settingsFacade.Grid; + _themeSettingsService = _settingsFacade.Theme; + _weatherSettingsService = _settingsFacade.Weather; + _regionSettingsService = _settingsFacade.Region; + _updateSettingsService = _settingsFacade.Update; + _timeZoneService = _regionSettingsService.GetTimeZoneService(); + _weatherDataService = _weatherSettingsService.GetWeatherInfoService(); InitializeComponent(); - InitializePluginSettingsNavigation(); _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( _componentRegistry, pluginRuntimeService); + _componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry); _fluentAvaloniaTheme = Application.Current?.Styles.OfType().FirstOrDefault(); - AppSettingsService.SettingsSaved += OnExternalAppSettingsSaved; - LauncherSettingsService.SettingsSaved += OnExternalLauncherSettingsSaved; - PendingRestartStateService.StateChanged += OnPendingRestartStateChanged; + _settingsService.Changed += OnSettingsChanged; PropertyChanged += OnWindowPropertyChanged; InitializeDesktopSurfaceSwipeHandlers(); InitializeDesktopComponentDragHandlers(); - - PickWallpaperButton.Click += OnPickWallpaperClick; - ClearWallpaperButton.Click += OnClearWallpaperClick; - WallpaperPlacementComboBox.SelectionChanged += OnWallpaperPlacementSelectionChanged; - - GridSizeSlider.ValueChanged += OnGridSizeSliderChanged; - GridSpacingPresetComboBox.SelectionChanged += OnGridSpacingPresetSelectionChanged; - GridEdgeInsetSlider.ValueChanged += OnGridEdgeInsetSliderChanged; - ApplyGridButton.Click += OnApplyGridSizeClick; - - NightModeToggleSwitch.IsCheckedChanged += OnNightModeIsCheckedChanged; - RecommendedColorButton1.Click += OnRecommendedColorClick; - RecommendedColorButton2.Click += OnRecommendedColorClick; - RecommendedColorButton3.Click += OnRecommendedColorClick; - RecommendedColorButton4.Click += OnRecommendedColorClick; - RecommendedColorButton5.Click += OnRecommendedColorClick; - RecommendedColorButton6.Click += OnRecommendedColorClick; - RefreshMonetColorsButton.Click += OnRefreshMonetColorsClick; - MonetColorButton1.Click += OnMonetColorClick; - MonetColorButton2.Click += OnMonetColorClick; - MonetColorButton3.Click += OnMonetColorClick; - MonetColorButton4.Click += OnMonetColorClick; - MonetColorButton5.Click += OnMonetColorClick; - MonetColorButton6.Click += OnMonetColorClick; - - StatusBarClockToggleSwitch.IsCheckedChanged += OnStatusBarClockIsCheckedChanged; - ClockFormatHMSSRadio.IsCheckedChanged += OnClockFormatChanged; - ClockFormatHMRadio.IsCheckedChanged += OnClockFormatChanged; - StatusBarSpacingModeComboBox.SelectionChanged += OnStatusBarSpacingModeChanged; - StatusBarSpacingSlider.ValueChanged += OnStatusBarSpacingSliderChanged; - - WeatherPreviewButton.Click += OnTestWeatherRequestClick; - WeatherLocationModeComboBox.SelectionChanged += OnWeatherLocationModeSelectionChanged; - WeatherLocationModeChipListBox.SelectionChanged += OnWeatherLocationModeChipSelectionChanged; - WeatherAutoRefreshToggleSwitch.IsCheckedChanged += OnWeatherAutoRefreshToggled; - WeatherSearchButton.Click += OnSearchWeatherCityClick; - WeatherApplyCityButton.Click += OnApplyWeatherCitySelectionClick; - WeatherApplyCoordinatesButton.Click += OnApplyWeatherCoordinatesClick; - WeatherExcludedAlertsTextBox.LostFocus += OnWeatherExcludedAlertsLostFocus; - WeatherIconPackComboBox.SelectionChanged += OnWeatherIconPackSelectionChanged; - WeatherNoTlsToggleSwitch.IsCheckedChanged += OnWeatherNoTlsToggled; - - LanguageComboBox.SelectionChanged += OnLanguageSelectionChanged; - TimeZoneComboBox.SelectionChanged += OnTimeZoneSelectionChanged; - - AutoCheckUpdatesToggleSwitch.IsCheckedChanged += OnAutoCheckUpdatesToggled; - UpdateChannelChipListBox.SelectionChanged += OnUpdateChannelSelectionChanged; - CheckForUpdatesButton.Click += OnCheckForUpdatesClick; - DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick; - - AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled; - AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged; } private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e) @@ -275,9 +236,9 @@ public partial class MainWindow : Window base.OnOpened(e); _suppressSettingsPersistence = true; - var snapshot = _appSettingsService.Load(); - var desktopLayoutSnapshot = _desktopLayoutSettingsService.Load(); - var launcherSnapshot = _launcherSettingsService.Load(); + var snapshot = _settingsService.LoadSnapshot(SettingsScope.App); + var desktopLayoutSnapshot = _componentLayoutStore.LoadLayout(); + var launcherSnapshot = _settingsService.LoadSnapshot(SettingsScope.Launcher); if (!string.IsNullOrWhiteSpace(snapshot.TimeZoneId)) { @@ -289,47 +250,16 @@ public partial class MainWindow : Window MinShortSideCells, MaxShortSideCells); - _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); - _suppressGridSpacingEvents = true; - GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0; - _suppressGridSpacingEvents = false; + _gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset); _desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent); - _suppressGridInsetEvents = true; - GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent; - GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent; - _suppressGridInsetEvents = false; - GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged; _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode); _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30); - _suppressStatusBarSpacingEvents = true; - StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch - { - "Compact" => 0, - "Custom" => 2, - _ => 1 - }; - StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent; - StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent; - StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase); - _suppressStatusBarSpacingEvents = false; - StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged; - - GridSizeNumberBox.Value = _targetShortSideCells; - GridSizeSlider.Value = _targetShortSideCells; - GridSizeSlider.ValueChanged += OnGridSizeSliderChanged; - GridSizeNumberBox.ValueChanged += OnGridSizeNumberBoxChanged; - - RestoreSettingsTabSelection(snapshot); - UpdateSettingsTabContent(); - - WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement); _defaultDesktopBackground = DesktopWallpaperLayer.Background; ApplyTaskbarSettings(snapshot); InitializeLocalization(snapshot.LanguageCode); InitializeWeatherSettings(snapshot); - _ = _componentSettingsService.Load(); InitializeAutoStartWithWindowsSetting(snapshot); InitializeAppRenderModeSetting(snapshot); InitializeUpdateSettings(snapshot); @@ -349,15 +279,8 @@ public partial class MainWindow : Window _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); ApplyNightModeState(_isNightMode, refreshPalettes: true); - _suppressStatusBarToggleEvents = true; - StatusBarClockToggleSwitch.IsChecked = _topStatusComponentIds.Contains(BuiltInComponentIds.Clock); - _suppressStatusBarToggleEvents = false; ApplyLocalization(); - ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor); - _settingsContentPanelTransform = SettingsContentPanel.RenderTransform as TranslateTransform; DesktopHost.SizeChanged += OnDesktopHostSizeChanged; - WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged; - GridPreviewHost.SizeChanged += OnGridPreviewHostSizeChanged; RebuildDesktopGrid(); LoadLauncherEntriesAsync(); InitializeTimeZoneSettings(); @@ -384,28 +307,15 @@ public partial class MainWindow : Window _wallpaperPreviewSnapshotBitmap = null; _libVlc?.Dispose(); _libVlc = null; - if (_weatherDataService is IDisposable weatherServiceDisposable) - { - weatherServiceDisposable.Dispose(); - } if (_recommendationInfoService is IDisposable recommendationServiceDisposable) { recommendationServiceDisposable.Dispose(); } - _releaseUpdateService.Dispose(); _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; - AppSettingsService.SettingsSaved -= OnExternalAppSettingsSaved; - LauncherSettingsService.SettingsSaved -= OnExternalLauncherSettingsSaved; - PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged; + _settingsService.Changed -= OnSettingsChanged; PropertyChanged -= OnWindowPropertyChanged; DesktopHost.SizeChanged -= OnDesktopHostSizeChanged; - WallpaperPreviewHost.SizeChanged -= OnWallpaperPreviewHostSizeChanged; - GridPreviewHost.SizeChanged -= OnGridPreviewHostSizeChanged; - GridSizeSlider.ValueChanged -= OnGridSizeSliderChanged; - GridSizeNumberBox.ValueChanged -= OnGridSizeNumberBoxChanged; - GridEdgeInsetNumberBox.ValueChanged -= OnGridEdgeInsetNumberBoxChanged; - StatusBarSpacingNumberBox.ValueChanged -= OnStatusBarSpacingNumberBoxChanged; base.OnClosed(e); } @@ -434,6 +344,11 @@ public partial class MainWindow : Window private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e) { + if (GridSizeSlider is null || GridSizeNumberBox is null) + { + return; + } + var sliderValue = (int)Math.Round(GridSizeSlider.Value); if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon) { @@ -444,6 +359,11 @@ public partial class MainWindow : Window private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) { + if (GridSizeSlider is null || GridSizeNumberBox is null) + { + return; + } + var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value); if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon) { @@ -454,6 +374,11 @@ public partial class MainWindow : Window private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e) { + if (GridEdgeInsetSlider is null) + { + return; + } + if (_suppressGridInsetEvents) { return; @@ -466,6 +391,11 @@ public partial class MainWindow : Window private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) { + if (GridEdgeInsetNumberBox is null) + { + return; + } + if (_suppressGridInsetEvents) { return; @@ -511,6 +441,11 @@ public partial class MainWindow : Window private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e) { + if (StatusBarSpacingModeComboBox is null) + { + return; + } + if (_suppressStatusBarSpacingEvents) { return; @@ -529,6 +464,11 @@ public partial class MainWindow : Window private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e) { + if (StatusBarSpacingSlider is null) + { + return; + } + if (_suppressStatusBarSpacingEvents) { return; @@ -549,6 +489,11 @@ public partial class MainWindow : Window private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) { + if (StatusBarSpacingNumberBox is null) + { + return; + } + if (_suppressStatusBarSpacingEvents) { return; @@ -597,7 +542,8 @@ public partial class MainWindow : Window GridPreviewHost is null || GridPreviewViewport is null || GridPreviewGrid is null || - GridPreviewLinesCanvas is null) + GridPreviewLinesCanvas is null || + GridSizeSlider is null) { return; } @@ -626,11 +572,11 @@ public partial class MainWindow : Window var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding); var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding); - var preset = _gridLayoutService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); - var gapRatio = _gridLayoutService.ResolveGapRatio(preset); + var preset = _gridSettingsService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); + var gapRatio = _gridSettingsService.ResolveGapRatio(preset); var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); - var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); - var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); + var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); + var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; @@ -676,12 +622,15 @@ public partial class MainWindow : Window ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize); UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize); - GridInfoTextBlock.Text = Lf( - "settings.grid.info_format", - "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", - gridMetrics.ColumnCount, - gridMetrics.RowCount, - gridMetrics.CellSize); + if (GridInfoTextBlock is not null) + { + GridInfoTextBlock.Text = Lf( + "settings.grid.info_format", + "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", + gridMetrics.ColumnCount, + gridMetrics.RowCount, + gridMetrics.CellSize); + } DrawGridPreviewLines(gridMetrics); } @@ -767,7 +716,12 @@ public partial class MainWindow : Window private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) { - _gridSpacingPreset = _gridLayoutService.NormalizeSpacingPreset( + if (GridSizeNumberBox is null || GridSizeSlider is null) + { + return; + } + + _gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset( TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); _desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); @@ -825,9 +779,9 @@ public partial class MainWindow : Window { var hostWidth = DesktopHost.Bounds.Width; var hostHeight = DesktopHost.Bounds.Height; - var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); - var edgeInset = _gridLayoutService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent); - var gridMetrics = _gridLayoutService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset); + var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset); + var edgeInset = _gridSettingsService.CalculateEdgeInset(hostWidth, hostHeight, _targetShortSideCells, _desktopEdgeInsetPercent); + var gridMetrics = _gridSettingsService.CalculateGridMetrics(hostWidth, hostHeight, _targetShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; @@ -875,12 +829,15 @@ public partial class MainWindow : Window UpdateDesktopSurfaceLayout(gridMetrics); UpdateSettingsViewportInsets(gridMetrics.CellSize); - GridInfoTextBlock.Text = Lf( - "settings.grid.info_format", - "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", - gridMetrics.ColumnCount, - gridMetrics.RowCount, - gridMetrics.CellSize); + if (GridInfoTextBlock is not null) + { + GridInfoTextBlock.Text = Lf( + "settings.grid.info_format", + "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", + gridMetrics.ColumnCount, + gridMetrics.RowCount, + gridMetrics.CellSize); + } UpdateWallpaperPreviewLayout(); } @@ -930,6 +887,11 @@ public partial class MainWindow : Window private int ResolvePendingGridEdgeInsetPercent() { + if (GridEdgeInsetNumberBox is null) + { + return _desktopEdgeInsetPercent; + } + var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value); return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent); } @@ -1067,47 +1029,7 @@ public partial class MainWindow : Window private void UpdateSettingsViewportInsets(double cellSize) { - if (SettingsContentPanel is null) - { - return; - } - - var clampedCell = Math.Max(1, cellSize); - var horizontalInset = Math.Clamp(clampedCell * 0.45, 12, 64); - var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18); - var edgeInset = Math.Max(0, _currentDesktopEdgeInset); - - var taskbarCellHeight = Math.Clamp(clampedCell * 0.76, 36, 76); - var taskbarPadding = Math.Clamp(taskbarCellHeight * 0.16, 6, 14); - var taskbarVisualHeight = Math.Max(clampedCell, taskbarCellHeight + taskbarPadding * 2); - if (BottomTaskbarContainer is not null && BottomTaskbarContainer.Bounds.Height > 1) - { - taskbarVisualHeight = Math.Max(taskbarVisualHeight, BottomTaskbarContainer.Bounds.Height); - } - - var statusBarVisualHeight = clampedCell; - if (TopStatusBarHost is not null && TopStatusBarHost.Bounds.Height > 1) - { - statusBarVisualHeight = Math.Max(statusBarVisualHeight, TopStatusBarHost.Bounds.Height); - } - - var topInset = Math.Max(clampedCell + verticalGap, edgeInset + statusBarVisualHeight + verticalGap); - var bottomInset = Math.Max(clampedCell + verticalGap, edgeInset + taskbarVisualHeight + verticalGap); - - // Add extra safety margin so rounded panel corners never clip against viewport edges. - var cornerSafetyMargin = Math.Clamp(clampedCell * 0.12, 4, 12); - var inset = new Thickness( - horizontalInset + cornerSafetyMargin, - topInset + cornerSafetyMargin, - horizontalInset + cornerSafetyMargin, - bottomInset + cornerSafetyMargin); - - // Keep panel stretched with explicit viewport insets so it never overlaps fixed chrome. - SettingsContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch; - SettingsContentPanel.VerticalAlignment = VerticalAlignment.Stretch; - SettingsContentPanel.Margin = inset; - SettingsContentPanel.Width = double.NaN; - SettingsContentPanel.Height = double.NaN; + _ = cellSize; } private void UpdateWallpaperPreviewLayout() @@ -1160,9 +1082,9 @@ public partial class MainWindow : Window var innerWidth = Math.Max(1, previewWidth - horizontalPadding); var innerHeight = Math.Max(1, previewHeight - verticalPadding); - var gapRatio = _gridLayoutService.ResolveGapRatio(_gridSpacingPreset); - var edgeInset = _gridLayoutService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); - var gridMetrics = _gridLayoutService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); + var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset); + var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); + var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); if (gridMetrics.CellSize <= 0) { return; @@ -1273,6 +1195,11 @@ public partial class MainWindow : Window private void InitializeTimeZoneSettings() { + if (TimeZoneComboBox is null) + { + return; + } + // Populate timezone dropdown items before selecting current timezone. _suppressTimeZoneSelectionEvents = true; TimeZoneComboBox.Items.Clear(); @@ -1299,7 +1226,9 @@ public partial class MainWindow : Window private void OnTimeZoneSelectionChanged(object? sender, SelectionChangedEventArgs e) { - if (_suppressTimeZoneSelectionEvents || TimeZoneComboBox.SelectedItem is not ComboBoxItem item) + if (TimeZoneComboBox is null || + _suppressTimeZoneSelectionEvents || + TimeZoneComboBox.SelectedItem is not ComboBoxItem item) { return; } diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml deleted file mode 100644 index fe9534b..0000000 --- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml +++ /dev/null @@ -1,80 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs deleted file mode 100644 index 3acf86e..0000000 --- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace LanMountainDesktop.Views.SettingsPages; - -public partial class AboutSettingsPage : UserControl -{ - public AboutSettingsPage() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml deleted file mode 100644 index 1501242..0000000 --- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs deleted file mode 100644 index cefb969..0000000 --- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace LanMountainDesktop.Views.SettingsPages; - -public partial class AppearanceSettingsPage : UserControl -{ - public AppearanceSettingsPage() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml deleted file mode 100644 index c0835aa..0000000 --- a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml +++ /dev/null @@ -1,218 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml.cs deleted file mode 100644 index 33279a8..0000000 --- a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml.cs +++ /dev/null @@ -1,12 +0,0 @@ -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace LanMountainDesktop.Views.SettingsPages; - -public partial class ColorSettingsPage : UserControl -{ - public ColorSettingsPage() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml deleted file mode 100644 index 5d2c94c..0000000 --- a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml +++ /dev/null @@ -1,48 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs deleted file mode 100644 index 3a4ae8d..0000000 --- a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace LanMountainDesktop.Views.SettingsPages; - -public partial class ComponentsSettingsPage : UserControl -{ - public ComponentsSettingsPage() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml deleted file mode 100644 index 7102a9a..0000000 --- a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml +++ /dev/null @@ -1,58 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs deleted file mode 100644 index dbd74e2..0000000 --- a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Avalonia.Controls; - -namespace LanMountainDesktop.Views.SettingsPages; - -public partial class GeneralSettingsPage : UserControl -{ - public GeneralSettingsPage() - { - InitializeComponent(); - } -} diff --git a/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml deleted file mode 100644 index 4cb7962..0000000 --- a/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml +++ /dev/null @@ -1,175 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -