using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using System.Threading; using System.Threading.Tasks; using Avalonia.Media; using Avalonia.Media.Imaging; using LanMountainDesktop.Models; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Settings.Core; using LanMountainDesktop.Services.PluginMarket; namespace LanMountainDesktop.Services.Settings; internal sealed class GridSettingsService : IGridSettingsService { private readonly ISettingsService _settingsService; private readonly DesktopGridLayoutService _gridLayoutService = new(); public GridSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public GridSettingsState Get() { var snapshot = _settingsService.Load(); return new GridSettingsState( snapshot.GridShortSideCells, snapshot.GridSpacingPreset, snapshot.DesktopEdgeInsetPercent); } public void Save(GridSettingsState state) { var snapshot = _settingsService.Load(); snapshot.GridShortSideCells = state.ShortSideCells; snapshot.GridSpacingPreset = state.SpacingPreset; snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent; _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.GridShortSideCells), nameof(AppSettingsSnapshot.GridSpacingPreset), nameof(AppSettingsSnapshot.DesktopEdgeInsetPercent) ]); } 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 { private readonly ISettingsService _settingsService; public WallpaperSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public WallpaperSettingsState Get() { var snapshot = _settingsService.Load(); var normalizedType = snapshot.WallpaperType ?? "Image"; return new WallpaperSettingsState( string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase) ? null : snapshot.WallpaperPath, normalizedType, snapshot.WallpaperColor, snapshot.WallpaperPlacement, CustomColor: null, SystemWallpaperRefreshIntervalSeconds: NormalizeRefreshInterval(snapshot.SystemWallpaperRefreshIntervalSeconds)); } public void Save(WallpaperSettingsState state) { var snapshot = _settingsService.Load(); var normalizedType = string.IsNullOrWhiteSpace(state.Type) ? "Image" : state.Type.Trim(); var normalizedPath = string.IsNullOrWhiteSpace(state.WallpaperPath) ? null : state.WallpaperPath.Trim(); var normalizedColor = string.IsNullOrWhiteSpace(state.Color) ? null : state.Color.Trim(); if (string.Equals(normalizedType, "SolidColor", StringComparison.OrdinalIgnoreCase)) { normalizedPath = null; } snapshot.WallpaperPath = normalizedPath; snapshot.WallpaperType = normalizedType; snapshot.WallpaperColor = normalizedColor; snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement) ? "Fill" : state.Placement.Trim(); snapshot.SystemWallpaperRefreshIntervalSeconds = NormalizeRefreshInterval(state.SystemWallpaperRefreshIntervalSeconds); _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.WallpaperPath), nameof(AppSettingsSnapshot.WallpaperType), nameof(AppSettingsSnapshot.WallpaperColor), nameof(AppSettingsSnapshot.WallpaperPlacement), nameof(AppSettingsSnapshot.SystemWallpaperRefreshIntervalSeconds) ]); } private static int NormalizeRefreshInterval(int seconds) { return seconds switch { <= 0 => 300, < 30 => 30, > 86400 => 86400, _ => seconds }; } } internal sealed class WallpaperMediaService : IWallpaperMediaService { private static readonly HashSet ImageExtensions = new(StringComparer.OrdinalIgnoreCase) { ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" }; private readonly string _wallpapersDirectory; public WallpaperMediaService() { var appDataRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LanMountainDesktop"); _wallpapersDirectory = Path.Combine(appDataRoot, "Wallpapers"); } public WallpaperMediaType DetectMediaType(string? path) { if (string.IsNullOrWhiteSpace(path)) { return WallpaperMediaType.None; } var extension = Path.GetExtension(path.Trim()); if (string.IsNullOrWhiteSpace(extension)) { return WallpaperMediaType.None; } if (ImageExtensions.Contains(extension)) { return WallpaperMediaType.Image; } return WallpaperMediaType.None; } public async Task ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(sourcePath)) { return null; } var fullSourcePath = Path.GetFullPath(sourcePath); if (!File.Exists(fullSourcePath)) { return null; } if (DetectMediaType(fullSourcePath) == WallpaperMediaType.None) { return null; } Directory.CreateDirectory(_wallpapersDirectory); var extension = Path.GetExtension(fullSourcePath); var baseName = Path.GetFileNameWithoutExtension(fullSourcePath); var normalizedBaseName = string.IsNullOrWhiteSpace(baseName) ? "wallpaper" : string.Concat(baseName.Select(ch => Path.GetInvalidFileNameChars().Contains(ch) ? '_' : ch)); var destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}{extension}"); if (string.Equals(fullSourcePath, destinationPath, StringComparison.OrdinalIgnoreCase)) { return destinationPath; } var suffix = 1; while (File.Exists(destinationPath)) { destinationPath = Path.Combine(_wallpapersDirectory, $"{normalizedBaseName}_{suffix}{extension}"); suffix++; } await using var source = File.OpenRead(fullSourcePath); await using var destination = File.Create(destinationPath); await source.CopyToAsync(destination, cancellationToken); return destinationPath; } } internal sealed class ThemeAppearanceService : IThemeAppearanceService { private readonly ISettingsService _settingsService; private readonly MonetColorService _monetColorService = new(); private readonly WallpaperMediaService _wallpaperMediaService = new(); public ThemeAppearanceService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public ThemeAppearanceSettingsState Get() { var snapshot = _settingsService.Load(); var cornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle); if (string.Equals(cornerRadiusStyle, GlobalAppearanceSettings.DefaultCornerRadiusStyle, StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(snapshot.CornerRadiusStyle) && Math.Abs(snapshot.GlobalCornerRadiusScale - GlobalAppearanceSettings.DefaultCornerRadiusScale) > 0.01) { cornerRadiusStyle = GlobalAppearanceSettings.MigrateScaleToStyle(snapshot.GlobalCornerRadiusScale); } return new ThemeAppearanceSettingsState( snapshot.IsNightMode ?? false, snapshot.ThemeColor, snapshot.UseSystemChrome, cornerRadiusStyle, ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor), ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode), snapshot.SelectedWallpaperSeed); } public void Save(ThemeAppearanceSettingsState state) { var snapshot = _settingsService.Load(); var changedKeys = new List(); var normalizedThemeColor = string.IsNullOrWhiteSpace(state.ThemeColor) ? null : state.ThemeColor; var normalizedCornerRadiusStyle = GlobalAppearanceSettings.NormalizeCornerRadiusStyle(state.CornerRadiusStyle); var normalizedThemeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(state.ThemeColorMode, state.ThemeColor); var normalizedSystemMaterialMode = ThemeAppearanceValues.NormalizeSystemMaterialMode(state.SystemMaterialMode); var normalizedSelectedWallpaperSeed = string.IsNullOrWhiteSpace(state.SelectedWallpaperSeed) ? null : state.SelectedWallpaperSeed; if ((snapshot.IsNightMode ?? false) != state.IsNightMode) { snapshot.IsNightMode = state.IsNightMode; changedKeys.Add(nameof(AppSettingsSnapshot.IsNightMode)); } if (!string.Equals(snapshot.ThemeColor, normalizedThemeColor, StringComparison.OrdinalIgnoreCase)) { snapshot.ThemeColor = normalizedThemeColor; changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColor)); } if (snapshot.UseSystemChrome != state.UseSystemChrome) { snapshot.UseSystemChrome = state.UseSystemChrome; changedKeys.Add(nameof(AppSettingsSnapshot.UseSystemChrome)); } if (!string.Equals(GlobalAppearanceSettings.NormalizeCornerRadiusStyle(snapshot.CornerRadiusStyle), normalizedCornerRadiusStyle, StringComparison.OrdinalIgnoreCase)) { snapshot.CornerRadiusStyle = normalizedCornerRadiusStyle; changedKeys.Add(nameof(AppSettingsSnapshot.CornerRadiusStyle)); } if (!string.Equals(snapshot.ThemeColorMode, normalizedThemeColorMode, StringComparison.OrdinalIgnoreCase)) { snapshot.ThemeColorMode = normalizedThemeColorMode; changedKeys.Add(nameof(AppSettingsSnapshot.ThemeColorMode)); } if (!string.Equals(snapshot.SystemMaterialMode, normalizedSystemMaterialMode, StringComparison.OrdinalIgnoreCase)) { snapshot.SystemMaterialMode = normalizedSystemMaterialMode; changedKeys.Add(nameof(AppSettingsSnapshot.SystemMaterialMode)); } if (!string.Equals(snapshot.SelectedWallpaperSeed, normalizedSelectedWallpaperSeed, StringComparison.OrdinalIgnoreCase)) { snapshot.SelectedWallpaperSeed = normalizedSelectedWallpaperSeed; changedKeys.Add(nameof(AppSettingsSnapshot.SelectedWallpaperSeed)); } if (changedKeys.Count == 0) { return; } _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: changedKeys); } public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null) { Bitmap? bitmap = null; Color? preferredSeed = null; if (!string.IsNullOrWhiteSpace(preferredSeedColor) && Color.TryParse(preferredSeedColor, out var parsedSeed)) { preferredSeed = parsedSeed; } try { if (_wallpaperMediaService.DetectMediaType(wallpaperPath) == WallpaperMediaType.Image && !string.IsNullOrWhiteSpace(wallpaperPath) && File.Exists(wallpaperPath)) { bitmap = new Bitmap(wallpaperPath); } } catch (Exception ex) { AppLogger.Warn( "Settings.Theme", $"Failed to load wallpaper bitmap for palette generation. Path='{wallpaperPath}'.", ex); } try { return _monetColorService.BuildPalette(bitmap, nightMode, preferredSeed); } finally { bitmap?.Dispose(); } } } internal sealed class StatusBarSettingsService : IStatusBarSettingsService { private readonly ISettingsService _settingsService; public StatusBarSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public StatusBarSettingsState Get() { var snapshot = _settingsService.Load(); return new StatusBarSettingsState( snapshot.TopStatusComponentIds?.ToArray() ?? [], snapshot.PinnedTaskbarActions?.ToArray() ?? [], snapshot.EnableDynamicTaskbarActions, snapshot.TaskbarLayoutMode, snapshot.ClockDisplayFormat, snapshot.StatusBarClockTransparentBackground, snapshot.ClockPosition, snapshot.ClockFontSize, snapshot.ShowTextCapsule, snapshot.TextCapsuleContent, snapshot.TextCapsulePosition, snapshot.TextCapsuleTransparentBackground, snapshot.TextCapsuleFontSize, snapshot.ShowNetworkSpeed, snapshot.NetworkSpeedPosition, snapshot.NetworkSpeedDisplayMode, snapshot.NetworkSpeedTransparentBackground, snapshot.ShowNetworkTypeIcon, snapshot.NetworkSpeedFontSize, snapshot.StatusBarSpacingMode, snapshot.StatusBarCustomSpacingPercent, snapshot.StatusBarShadowEnabled, snapshot.StatusBarShadowColor, snapshot.StatusBarShadowOpacity); } public void Save(StatusBarSettingsState state) { var snapshot = _settingsService.Load(); snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? []; snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? []; snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions; snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode; snapshot.ClockDisplayFormat = state.ClockDisplayFormat; snapshot.StatusBarClockTransparentBackground = state.ClockTransparentBackground; snapshot.ClockPosition = state.ClockPosition; snapshot.ClockFontSize = state.ClockFontSize; snapshot.ShowTextCapsule = state.ShowTextCapsule; snapshot.TextCapsuleContent = state.TextCapsuleContent; snapshot.TextCapsulePosition = state.TextCapsulePosition; snapshot.TextCapsuleTransparentBackground = state.TextCapsuleTransparentBackground; snapshot.TextCapsuleFontSize = state.TextCapsuleFontSize; snapshot.ShowNetworkSpeed = state.ShowNetworkSpeed; snapshot.NetworkSpeedPosition = state.NetworkSpeedPosition; snapshot.NetworkSpeedDisplayMode = state.NetworkSpeedDisplayMode; snapshot.NetworkSpeedTransparentBackground = state.NetworkSpeedTransparentBackground; snapshot.ShowNetworkTypeIcon = state.ShowNetworkTypeIcon; snapshot.NetworkSpeedFontSize = state.NetworkSpeedFontSize; snapshot.StatusBarSpacingMode = state.SpacingMode; snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent; snapshot.StatusBarShadowEnabled = state.ShadowEnabled; snapshot.StatusBarShadowColor = state.ShadowColor; snapshot.StatusBarShadowOpacity = state.ShadowOpacity; _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.TopStatusComponentIds), nameof(AppSettingsSnapshot.PinnedTaskbarActions), nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions), nameof(AppSettingsSnapshot.TaskbarLayoutMode), nameof(AppSettingsSnapshot.ClockDisplayFormat), nameof(AppSettingsSnapshot.StatusBarClockTransparentBackground), nameof(AppSettingsSnapshot.ClockPosition), nameof(AppSettingsSnapshot.ClockFontSize), nameof(AppSettingsSnapshot.ShowTextCapsule), nameof(AppSettingsSnapshot.TextCapsuleContent), nameof(AppSettingsSnapshot.TextCapsulePosition), nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground), nameof(AppSettingsSnapshot.TextCapsuleFontSize), nameof(AppSettingsSnapshot.ShowNetworkSpeed), nameof(AppSettingsSnapshot.NetworkSpeedPosition), nameof(AppSettingsSnapshot.NetworkSpeedDisplayMode), nameof(AppSettingsSnapshot.NetworkSpeedTransparentBackground), nameof(AppSettingsSnapshot.ShowNetworkTypeIcon), nameof(AppSettingsSnapshot.NetworkSpeedFontSize), nameof(AppSettingsSnapshot.StatusBarSpacingMode), nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent), nameof(AppSettingsSnapshot.StatusBarShadowEnabled), nameof(AppSettingsSnapshot.StatusBarShadowColor), nameof(AppSettingsSnapshot.StatusBarShadowOpacity) ]); } } internal sealed class TextCapsuleSettingsService : ITextCapsuleSettingsService { private readonly ISettingsService _settingsService; public TextCapsuleSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public TextCapsuleSettingsState Get() { var snapshot = _settingsService.Load(); return new TextCapsuleSettingsState( snapshot.ShowTextCapsule, snapshot.TextCapsuleContent, snapshot.TextCapsulePosition, snapshot.TextCapsuleTransparentBackground); } public void Save(TextCapsuleSettingsState state) { var snapshot = _settingsService.Load(); snapshot.ShowTextCapsule = state.ShowTextCapsule; snapshot.TextCapsuleContent = state.Content; snapshot.TextCapsulePosition = state.Position; snapshot.TextCapsuleTransparentBackground = state.TransparentBackground; _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.ShowTextCapsule), nameof(AppSettingsSnapshot.TextCapsuleContent), nameof(AppSettingsSnapshot.TextCapsulePosition), nameof(AppSettingsSnapshot.TextCapsuleTransparentBackground) ]); } } internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoService, IDisposable { private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService(); public Task>> SearchLocationsAsync( string keyword, string? locale = null, CancellationToken cancellationToken = default) { return _weatherDataService.SearchLocationsAsync(keyword, locale, cancellationToken); } public Task> GetWeatherAsync( WeatherQuery query, CancellationToken cancellationToken = default) { return _weatherDataService.GetWeatherAsync(query, cancellationToken); } public Task> ResolveLocationAsync( double latitude, double longitude, string? locale = null, CancellationToken cancellationToken = default) { return _weatherDataService.ResolveLocationAsync(latitude, longitude, locale, cancellationToken); } public void Dispose() { if (_weatherDataService is IDisposable disposable) { disposable.Dispose(); } } } internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposable { private readonly ISettingsService _settingsService; private readonly WeatherProviderAdapter _weatherProvider = new(); public WeatherSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public WeatherSettingsState Get() { var snapshot = _settingsService.Load(); return new WeatherSettingsState( snapshot.WeatherLocationMode, snapshot.WeatherLocationKey, snapshot.WeatherLocationName, snapshot.WeatherLatitude, snapshot.WeatherLongitude, snapshot.WeatherAutoRefreshLocation, snapshot.WeatherExcludedAlerts, NormalizeIconPackId(snapshot.WeatherIconPackId), snapshot.WeatherNoTlsRequests, snapshot.WeatherLocationQuery); } public void Save(WeatherSettingsState state) { var snapshot = _settingsService.Load(); snapshot.WeatherLocationMode = state.LocationMode; snapshot.WeatherLocationKey = state.LocationKey; snapshot.WeatherLocationName = state.LocationName; snapshot.WeatherLatitude = state.Latitude; snapshot.WeatherLongitude = state.Longitude; snapshot.WeatherAutoRefreshLocation = state.AutoRefreshLocation; snapshot.WeatherExcludedAlerts = state.ExcludedAlerts; snapshot.WeatherIconPackId = NormalizeIconPackId(state.IconPackId); snapshot.WeatherNoTlsRequests = state.NoTlsRequests; snapshot.WeatherLocationQuery = state.LocationQuery; _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.WeatherLocationMode), nameof(AppSettingsSnapshot.WeatherLocationKey), nameof(AppSettingsSnapshot.WeatherLocationName), nameof(AppSettingsSnapshot.WeatherLatitude), nameof(AppSettingsSnapshot.WeatherLongitude), nameof(AppSettingsSnapshot.WeatherAutoRefreshLocation), nameof(AppSettingsSnapshot.WeatherExcludedAlerts), nameof(AppSettingsSnapshot.WeatherIconPackId), nameof(AppSettingsSnapshot.WeatherNoTlsRequests), nameof(AppSettingsSnapshot.WeatherLocationQuery) ]); } public Task>> SearchLocationsAsync( string keyword, string? locale = null, CancellationToken cancellationToken = default) { return _weatherProvider.SearchLocationsAsync(keyword, locale, cancellationToken); } public Task> ResolveLocationAsync( double latitude, double longitude, string? locale = null, CancellationToken cancellationToken = default) { return _weatherProvider.ResolveLocationAsync(latitude, longitude, locale, cancellationToken); } public IWeatherInfoService GetWeatherInfoService() { return _weatherProvider; } public void Dispose() { _weatherProvider.Dispose(); } private static string NormalizeIconPackId(string? iconPackId) { return string.IsNullOrWhiteSpace(iconPackId) ? "HyperOS3" : "HyperOS3"; } } internal sealed class RegionSettingsService : IRegionSettingsService { private readonly ISettingsService _settingsService; private readonly TimeZoneService _timeZoneService = new(); public RegionSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); ApplyTimeZone(_settingsService.Load().TimeZoneId); } public RegionSettingsState Get() { var snapshot = _settingsService.Load(); return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId); } public void Save(RegionSettingsState state) { var snapshot = _settingsService.Load(); snapshot.LanguageCode = string.IsNullOrWhiteSpace(state.LanguageCode) ? "zh-CN" : state.LanguageCode.Trim(); snapshot.TimeZoneId = string.IsNullOrWhiteSpace(state.TimeZoneId) ? null : state.TimeZoneId.Trim(); _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.LanguageCode), nameof(AppSettingsSnapshot.TimeZoneId) ]); ApplyTimeZone(snapshot.TimeZoneId); } public TimeZoneService GetTimeZoneService() { return _timeZoneService; } private void ApplyTimeZone(string? timeZoneId) { if (string.IsNullOrWhiteSpace(timeZoneId)) { _timeZoneService.CurrentTimeZone = TimeZoneInfo.Local; return; } if (!_timeZoneService.SetTimeZoneById(timeZoneId)) { _timeZoneService.CurrentTimeZone = TimeZoneInfo.Local; } } } internal sealed class PrivacySettingsService : IPrivacySettingsService { private readonly ISettingsService _settingsService; public PrivacySettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public PrivacySettingsState Get() { var snapshot = _settingsService.Load(); return new PrivacySettingsState( snapshot.UploadAnonymousCrashData, snapshot.UploadAnonymousUsageData); } public void Save(PrivacySettingsState state) { var snapshot = _settingsService.Load(); var changedKeys = new List(); if (snapshot.UploadAnonymousCrashData != state.UploadAnonymousCrashData) { snapshot.UploadAnonymousCrashData = state.UploadAnonymousCrashData; changedKeys.Add(nameof(AppSettingsSnapshot.UploadAnonymousCrashData)); } if (snapshot.UploadAnonymousUsageData != state.UploadAnonymousUsageData) { snapshot.UploadAnonymousUsageData = state.UploadAnonymousUsageData; changedKeys.Add(nameof(AppSettingsSnapshot.UploadAnonymousUsageData)); } if (changedKeys.Count == 0) { return; } AppLogger.Info( "PrivacySettings", $"Saving: UploadAnonymousCrashData={state.UploadAnonymousCrashData}, UploadAnonymousUsageData={state.UploadAnonymousUsageData}"); _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: changedKeys); } } internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable { private readonly ISettingsService _settingsService; private readonly GitHubReleaseUpdateService _githubReleaseUpdateService = new("wwiinnddyy", "LanMountainDesktop"); private readonly PdcReleaseUpdateService _pdcReleaseUpdateService = new(); public UpdateSettingsService(ISettingsService settingsService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); } public UpdateSettingsState Get() { var snapshot = _settingsService.Load(); var normalizedChannel = UpdateSettingsValues.NormalizeChannel( snapshot.UpdateChannel, snapshot.IncludePrereleaseUpdates); return new UpdateSettingsState( string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase), normalizedChannel, UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode), UpdateSettingsValues.NormalizeDownloadSource(snapshot.UpdateDownloadSource), UpdateSettingsValues.NormalizeDownloadThreads(snapshot.UpdateDownloadThreads), snapshot.PendingUpdateInstallerPath, snapshot.PendingUpdateVersion, snapshot.PendingUpdatePublishedAtUtcMs, snapshot.LastUpdateCheckUtcMs, snapshot.PendingUpdateSha256); } public void Save(UpdateSettingsState state) { var snapshot = _settingsService.Load(); var normalizedChannel = UpdateSettingsValues.NormalizeChannel( state.UpdateChannel, state.IncludePrereleaseUpdates); snapshot.IncludePrereleaseUpdates = string.Equals( normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase); snapshot.UpdateChannel = normalizedChannel; snapshot.UpdateMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode); snapshot.UpdateDownloadSource = UpdateSettingsValues.NormalizeDownloadSource(state.UpdateDownloadSource); snapshot.UpdateDownloadThreads = UpdateSettingsValues.NormalizeDownloadThreads(state.UpdateDownloadThreads); snapshot.PendingUpdateInstallerPath = string.IsNullOrWhiteSpace(state.PendingUpdateInstallerPath) ? null : state.PendingUpdateInstallerPath.Trim(); snapshot.PendingUpdateVersion = string.IsNullOrWhiteSpace(state.PendingUpdateVersion) ? null : state.PendingUpdateVersion.Trim(); snapshot.PendingUpdatePublishedAtUtcMs = state.PendingUpdatePublishedAtUtcMs is > 0 ? state.PendingUpdatePublishedAtUtcMs : null; snapshot.LastUpdateCheckUtcMs = state.LastUpdateCheckUtcMs is > 0 ? state.LastUpdateCheckUtcMs : null; snapshot.PendingUpdateSha256 = string.IsNullOrWhiteSpace(state.PendingUpdateSha256) ? null : state.PendingUpdateSha256.Trim().ToLowerInvariant(); _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [ nameof(AppSettingsSnapshot.IncludePrereleaseUpdates), nameof(AppSettingsSnapshot.IncludePrereleaseUpdates), nameof(AppSettingsSnapshot.UpdateChannel), nameof(AppSettingsSnapshot.UpdateMode), nameof(AppSettingsSnapshot.UpdateDownloadSource), nameof(AppSettingsSnapshot.UpdateDownloadThreads), nameof(AppSettingsSnapshot.PendingUpdateInstallerPath), nameof(AppSettingsSnapshot.PendingUpdateVersion), nameof(AppSettingsSnapshot.PendingUpdatePublishedAtUtcMs), nameof(AppSettingsSnapshot.LastUpdateCheckUtcMs), nameof(AppSettingsSnapshot.PendingUpdateSha256) ]); } public Task CheckForUpdatesAsync( Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default) { return CheckForUpdatesCoreAsync(currentVersion, includePrerelease, isForce: false, cancellationToken); } public Task ForceCheckForUpdatesAsync( Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default) { return CheckForUpdatesCoreAsync(currentVersion, includePrerelease, isForce: true, cancellationToken); } public Task DownloadAssetAsync( GitHubReleaseAsset asset, string destinationFilePath, string downloadSource, int maxParallelSegments, IProgress? progress = null, CancellationToken cancellationToken = default) { return _githubReleaseUpdateService.DownloadAssetAsync( asset, destinationFilePath, downloadSource, maxParallelSegments, progress, cancellationToken); } public Task RedownloadAssetAsync( GitHubReleaseAsset asset, string destinationFilePath, string downloadSource, int maxParallelSegments, IProgress? progress = null, CancellationToken cancellationToken = default) { return _githubReleaseUpdateService.RedownloadAssetAsync( asset, destinationFilePath, downloadSource, maxParallelSegments, progress, cancellationToken); } public void Dispose() { _githubReleaseUpdateService.Dispose(); _pdcReleaseUpdateService.Dispose(); } private async Task CheckForUpdatesCoreAsync( Version currentVersion, bool includePrerelease, bool isForce, CancellationToken cancellationToken) { var source = UpdateSettingsValues.NormalizeDownloadSource(_settingsService.Load().UpdateDownloadSource); if (string.Equals(source, UpdateSettingsValues.DownloadSourcePdc, StringComparison.OrdinalIgnoreCase)) { var pdcResult = isForce ? await _pdcReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken) : await _pdcReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken); if (pdcResult.Success) { return pdcResult; } AppLogger.Warn( "UpdateSettings", $"PDC update check failed and will fallback to GitHub. Error: {pdcResult.ErrorMessage}"); } return isForce ? await _githubReleaseUpdateService.ForceCheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken) : await _githubReleaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken); } } internal sealed class LauncherCatalogService : ILauncherCatalogService { private readonly WindowsStartMenuService _startMenuService = new(); public StartMenuFolderNode LoadCatalog() { return _startMenuService.Load(); } } internal sealed class LauncherPolicyService : ILauncherPolicyService { private readonly LauncherSettingsService _launcherSettingsService = new(); public LauncherSettingsSnapshot Get() { return _launcherSettingsService.Load(); } public void Save(LauncherSettingsSnapshot snapshot) { _launcherSettingsService.Save(snapshot ?? new LauncherSettingsSnapshot()); } } internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService { private readonly ISettingsService _settingsService; private PluginRuntimeService? _pluginRuntimeService; public PluginManagementSettingsService(ISettingsService settingsService, PluginRuntimeService? pluginRuntimeService) { _settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService)); _pluginRuntimeService = pluginRuntimeService; } public void SetPluginRuntime(PluginRuntimeService? pluginRuntimeService) { _pluginRuntimeService = pluginRuntimeService; } public PluginManagementSettingsState Get() { var snapshot = _settingsService.Load(); return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []); } public void Save(PluginManagementSettingsState state) { var snapshot = _settingsService.Load(); snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? []; _settingsService.SaveSnapshot( SettingsScope.App, snapshot, changedKeys: [nameof(AppSettingsSnapshot.DisabledPluginIds)]); } public IReadOnlyList GetInstalledPlugins() { return _pluginRuntimeService?.GetInstalledPluginsSnapshot() ?? []; } public bool SetPluginEnabled(string pluginId, bool isEnabled) { return _pluginRuntimeService?.SetPluginEnabled(pluginId, isEnabled) ?? false; } public bool DeleteInstalledPlugin(string pluginId) { return _pluginRuntimeService?.DeleteInstalledPlugin(pluginId) ?? false; } } internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsService, IDisposable { private PluginRuntimeService? _pluginRuntimeService; private AirAppMarketIndexService _indexService; private AirAppMarketInstallService? _installService; private readonly Dictionary _cachedPlugins = new(StringComparer.OrdinalIgnoreCase); public PluginCatalogSettingsService(PluginRuntimeService? pluginRuntimeService) { _pluginRuntimeService = pluginRuntimeService; var dataRoot = Path.Combine( Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "LanMountainDesktop", "PluginMarket"); var cacheService = new AirAppMarketCacheService(dataRoot); _indexService = new AirAppMarketIndexService(cacheService); if (_pluginRuntimeService is not null) { _installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot); } } 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 Task LoadCatalogAsync(CancellationToken cancellationToken = default) { return LoadCatalogCoreAsync(cancellationToken); } public Task InstallAsync( string pluginId, CancellationToken cancellationToken = default) { return InstallCatalogCoreAsync(pluginId, cancellationToken); } private async Task LoadCatalogCoreAsync(CancellationToken cancellationToken = default) { var result = await _indexService.LoadAsync(cancellationToken).ConfigureAwait(false); var sources = BuildCatalogSources(result.Source?.ToString(), result.SourceLocation, result.WarningMessage); if (!result.Success || result.Document is null) { _cachedPlugins.Clear(); return new PluginCatalogIndexResult( false, [], sources, result.Source?.ToString(), result.SourceLocation, result.WarningMessage, result.ErrorMessage); } _cachedPlugins.Clear(); var plugins = result.Document.Plugins .Select(entry => { _cachedPlugins[entry.Id] = entry; return MapCatalogItem(entry); }) .ToArray(); return new PluginCatalogIndexResult( true, plugins, sources, result.Source?.ToString(), result.SourceLocation, result.WarningMessage, null); } private async Task InstallCatalogCoreAsync( string pluginId, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(pluginId)) { return new PluginCatalogInstallResult( false, null, null, null, [new PluginInstallDiagnostic("invalid_request", "Plugin id is required.")], "Plugin id is required."); } if (_installService is null || _pluginRuntimeService is null) { return new PluginCatalogInstallResult( false, pluginId, null, null, [new PluginInstallDiagnostic("runtime_unavailable", "Plugin runtime is unavailable.")], "Plugin runtime is unavailable."); } if (!_cachedPlugins.TryGetValue(pluginId, out var entry)) { var load = await LoadCatalogCoreAsync(cancellationToken).ConfigureAwait(false); if (!load.Success) { return new PluginCatalogInstallResult( false, pluginId, null, null, [new PluginInstallDiagnostic("catalog_load_failed", load.ErrorMessage ?? "Failed to load the plugin catalog.")], load.ErrorMessage); } if (!_cachedPlugins.TryGetValue(pluginId, out entry)) { return new PluginCatalogInstallResult( false, pluginId, null, null, [new PluginInstallDiagnostic("not_found", "Plugin was not found in the official catalog.")], "Plugin was not found in the official catalog."); } } var result = await _installService.InstallAsync(entry, cancellationToken).ConfigureAwait(false); if (!result.Success) { return new PluginCatalogInstallResult( false, entry.Id, entry.Name, null, [new PluginInstallDiagnostic("install_failed", result.ErrorMessage ?? "Plugin install failed.")], result.ErrorMessage); } return new PluginCatalogInstallResult( true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, result.Manifest, [], null); } private static PluginCatalogItemInfo MapCatalogItem(AirAppMarketPluginEntry entry) { var manifest = new PluginCatalogManifestInfo( entry.Id, entry.Name, entry.Description, entry.Author, entry.Version, entry.ApiVersion, string.Empty, entry.SharedContracts .Select(contract => new PluginCatalogSharedContractInfo( contract.Id, contract.Version, contract.AssemblyName)) .ToArray()); var compatibility = new PluginCatalogCompatibilityInfo( entry.MinHostVersion, entry.ApiVersion); var repository = new PluginCatalogRepositoryInfo( entry.IconUrl, entry.ProjectUrl, entry.ReadmeUrl, entry.HomepageUrl, entry.RepositoryUrl, entry.Tags.ToArray(), entry.ReleaseNotes); var publication = new PluginCatalogPublicationInfo( entry.ReleaseTag, entry.ReleaseAssetName, entry.PublishedAt, entry.UpdatedAt, entry.PackageSizeBytes, entry.Sha256, null); var sources = BuildPackageSources(entry); return new PluginCatalogItemInfo( manifest, compatibility, repository, publication, sources, []); } private static IReadOnlyList BuildPackageSources(AirAppMarketPluginEntry entry) { var sources = entry.GetPackageSourcesInInstallOrder(); if (sources.Count == 0) { return []; } return sources .Select(source => new PluginPackageSourceInfo( source.SourceKind switch { LanMountainDesktop.Services.PluginMarket.PluginPackageSourceKind.ReleaseAsset => PluginPackageSourceKind.ReleaseAsset, LanMountainDesktop.Services.PluginMarket.PluginPackageSourceKind.RawFallback => PluginPackageSourceKind.RawFallback, LanMountainDesktop.Services.PluginMarket.PluginPackageSourceKind.WorkspaceLocal => PluginPackageSourceKind.WorkspaceLocal, _ => PluginPackageSourceKind.RawFallback }, source.Url, entry.Sha256, entry.PackageSizeBytes)) .ToArray(); } private static IReadOnlyList BuildCatalogSources( string? sourceId, string? sourceLocation, string? warningMessage) { if (string.IsNullOrWhiteSpace(sourceId) && string.IsNullOrWhiteSpace(sourceLocation)) { return []; } var normalizedSourceId = string.IsNullOrWhiteSpace(sourceId) ? "plugin-catalog" : sourceId.Trim(); return [ new PluginCatalogSourceInfo( normalizedSourceId, normalizedSourceId, string.IsNullOrWhiteSpace(warningMessage) ? null : warningMessage.Trim(), string.IsNullOrWhiteSpace(sourceLocation) ? null : sourceLocation.Trim(), null, true, 0) ]; } public void Dispose() { _indexService.Dispose(); _installService?.Dispose(); } } internal sealed class ApplicationInfoService : IApplicationInfoService { private const string DefaultCodename = "Administrate"; public string GetAppVersionText() { // 优先从环境变量读取(Launcher 传递) var envVersion = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.VersionEnvVar); if (!string.IsNullOrWhiteSpace(envVersion)) { return envVersion; } // 回退:从程序集读取 var assembly = typeof(App).Assembly; var informationalVersion = assembly .GetCustomAttribute()? .InformationalVersion; if (!string.IsNullOrWhiteSpace(informationalVersion)) { var normalizedInformationalVersion = informationalVersion.Split('+', 2)[0].Trim(); if (!string.IsNullOrWhiteSpace(normalizedInformationalVersion)) { return normalizedInformationalVersion; } } var version = assembly.GetName().Version; if (version is null) { return "0.0.0"; } if (version.Revision >= 0) { return version.ToString(4); } if (version.Build >= 0) { return version.ToString(3); } if (version.Minor >= 0) { return version.ToString(2); } return version.ToString(); } public string GetAppCodenameText() { // 优先从环境变量读取(Launcher 传递) var envCodename = Environment.GetEnvironmentVariable(LanMountainDesktop.Shared.Contracts.Launcher.LauncherIpcConstants.CodenameEnvVar); if (!string.IsNullOrWhiteSpace(envCodename)) { return envCodename; } // 回退:使用默认开发代号 return DefaultCodename; } public AppRenderBackendInfo GetRenderBackendInfo() { return AppRenderBackendDiagnostics.Detect(); } } internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposable { private readonly UpdateSettingsService _updateSettingsService; private readonly PluginCatalogSettingsService _pluginCatalogSettingsService; private readonly PluginManagementSettingsService _pluginManagementSettingsService; private readonly WeatherSettingsService _weatherSettingsService; public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null) { Settings = new SettingsService(); Catalog = new SettingsCatalogService(); Grid = new GridSettingsService(Settings); Wallpaper = new WallpaperSettingsService(Settings); WallpaperMedia = new WallpaperMediaService(); Theme = new ThemeAppearanceService(Settings); StatusBar = new StatusBarSettingsService(Settings); TextCapsule = new TextCapsuleSettingsService(Settings); _weatherSettingsService = new WeatherSettingsService(Settings); Weather = _weatherSettingsService; Region = new RegionSettingsService(Settings); Privacy = new PrivacySettingsService(Settings); _updateSettingsService = new UpdateSettingsService(Settings); Update = _updateSettingsService; LauncherCatalog = new LauncherCatalogService(); LauncherPolicy = new LauncherPolicyService(); _pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService); PluginManagement = _pluginManagementSettingsService; _pluginCatalogSettingsService = new PluginCatalogSettingsService(pluginRuntimeService); PluginCatalog = _pluginCatalogSettingsService; ApplicationInfo = new ApplicationInfoService(); } public ISettingsService Settings { get; } public ISettingsCatalog Catalog { get; } public IGridSettingsService Grid { get; } public IWallpaperSettingsService Wallpaper { get; } public IWallpaperMediaService WallpaperMedia { get; } public IThemeAppearanceService Theme { get; } public IStatusBarSettingsService StatusBar { get; } public ITextCapsuleSettingsService TextCapsule { get; } public IWeatherSettingsService Weather { get; } public IRegionSettingsService Region { get; } public IPrivacySettingsService Privacy { get; } public IUpdateSettingsService Update { get; } public ILauncherCatalogService LauncherCatalog { get; } public ILauncherPolicyService LauncherPolicy { get; } public IPluginManagementSettingsService PluginManagement { get; } public IPluginCatalogSettingsService PluginCatalog { get; } public IApplicationInfoService ApplicationInfo { get; } public void BindPluginRuntime(PluginRuntimeService? pluginRuntimeService) { _pluginManagementSettingsService.SetPluginRuntime(pluginRuntimeService); _pluginCatalogSettingsService.SetPluginRuntime(pluginRuntimeService); } public void Dispose() { _weatherSettingsService.Dispose(); _updateSettingsService.Dispose(); _pluginCatalogSettingsService.Dispose(); } }