mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 01:44:26 +08:00
setting_re2
设置架构革新中
This commit is contained in:
116
LanMountainDesktop/Services/DesktopGridLayoutService.cs
Normal file
116
LanMountainDesktop/Services/DesktopGridLayoutService.cs
Normal file
@@ -0,0 +1,116 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
public readonly record struct DesktopGridMetrics(
|
||||
int ColumnCount,
|
||||
int RowCount,
|
||||
double CellSize,
|
||||
double GapPx,
|
||||
double EdgeInsetPx,
|
||||
double GridWidthPx,
|
||||
double GridHeightPx)
|
||||
{
|
||||
public double Pitch => CellSize + GapPx;
|
||||
}
|
||||
|
||||
public sealed class DesktopGridLayoutService
|
||||
{
|
||||
public const string RelaxedSpacingPreset = "Relaxed";
|
||||
public const string CompactSpacingPreset = "Compact";
|
||||
|
||||
public string NormalizeSpacingPreset(string? value)
|
||||
{
|
||||
return string.Equals(value, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase)
|
||||
? CompactSpacingPreset
|
||||
: RelaxedSpacingPreset;
|
||||
}
|
||||
|
||||
public double ResolveGapRatio(string? preset)
|
||||
{
|
||||
return string.Equals(preset, CompactSpacingPreset, StringComparison.OrdinalIgnoreCase) ? 0.06 : 0.12;
|
||||
}
|
||||
|
||||
public double CalculateEdgeInset(double hostWidth, double hostHeight, int shortSideCells, int insetPercent)
|
||||
{
|
||||
if (hostWidth <= 1 || hostHeight <= 1)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var cells = Math.Max(1, shortSideCells);
|
||||
var shortSidePx = Math.Max(1, Math.Min(hostWidth, hostHeight));
|
||||
var baseCell = shortSidePx / cells;
|
||||
var insetRatio = Math.Clamp(insetPercent, 0, 30) / 100d;
|
||||
return Math.Clamp(baseCell * insetRatio, 0, 80);
|
||||
}
|
||||
|
||||
public DesktopGridMetrics CalculateGridMetrics(
|
||||
double hostWidth,
|
||||
double hostHeight,
|
||||
int shortSideCells,
|
||||
double gapRatio,
|
||||
double edgeInsetPx)
|
||||
{
|
||||
if (hostWidth <= 1 || hostHeight <= 1)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var shortSide = Math.Max(1, shortSideCells);
|
||||
var clampedGapRatio = Math.Max(0, gapRatio);
|
||||
var inset = Math.Max(0, edgeInsetPx);
|
||||
var availableWidth = Math.Max(1, hostWidth - inset * 2);
|
||||
var availableHeight = Math.Max(1, hostHeight - inset * 2);
|
||||
|
||||
if (hostWidth >= hostHeight)
|
||||
{
|
||||
var rowCount = shortSide;
|
||||
var denominator = rowCount + Math.Max(0, rowCount - 1) * clampedGapRatio;
|
||||
if (denominator <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var cellSize = availableHeight / denominator;
|
||||
var gapPx = cellSize * clampedGapRatio;
|
||||
var pitch = cellSize + gapPx;
|
||||
if (pitch <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var columnCount = Math.Max(1, (int)Math.Floor((availableWidth + gapPx) / pitch));
|
||||
var gridWidth = columnCount * cellSize + Math.Max(0, columnCount - 1) * gapPx;
|
||||
var gridHeight = rowCount * cellSize + Math.Max(0, rowCount - 1) * gapPx;
|
||||
return new DesktopGridMetrics(columnCount, rowCount, cellSize, gapPx, inset, gridWidth, gridHeight);
|
||||
}
|
||||
|
||||
var columnCountPortrait = shortSide;
|
||||
var denominatorPortrait = columnCountPortrait + Math.Max(0, columnCountPortrait - 1) * clampedGapRatio;
|
||||
if (denominatorPortrait <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var cellSizePortrait = availableWidth / denominatorPortrait;
|
||||
var gapPxPortrait = cellSizePortrait * clampedGapRatio;
|
||||
var pitchPortrait = cellSizePortrait + gapPxPortrait;
|
||||
if (pitchPortrait <= 0)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
var rowCountPortrait = Math.Max(1, (int)Math.Floor((availableHeight + gapPxPortrait) / pitchPortrait));
|
||||
var gridWidthPortrait = columnCountPortrait * cellSizePortrait + Math.Max(0, columnCountPortrait - 1) * gapPxPortrait;
|
||||
var gridHeightPortrait = rowCountPortrait * cellSizePortrait + Math.Max(0, rowCountPortrait - 1) * gapPxPortrait;
|
||||
return new DesktopGridMetrics(
|
||||
columnCountPortrait,
|
||||
rowCountPortrait,
|
||||
cellSizePortrait,
|
||||
gapPxPortrait,
|
||||
inset,
|
||||
gridWidthPortrait,
|
||||
gridHeightPortrait);
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
using System;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Views;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
internal sealed class IndependentSettingsModuleService
|
||||
{
|
||||
private SettingsWindow? _window;
|
||||
|
||||
public void ShowOrActivate(string source, string? pageTag = null)
|
||||
{
|
||||
AppLogger.Info("IndependentSettingsModule", $"OpenRequested; Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
||||
|
||||
void ShowCore()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_window is not { } window)
|
||||
{
|
||||
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionStarted; Source='{source}'.");
|
||||
window = new SettingsWindow();
|
||||
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionCompleted; Source='{source}'.");
|
||||
window.Closed += (_, _) =>
|
||||
{
|
||||
if (ReferenceEquals(_window, window))
|
||||
{
|
||||
_window = null;
|
||||
}
|
||||
|
||||
AppLogger.Info("IndependentSettingsModule", "WindowClosed.");
|
||||
};
|
||||
_window = window;
|
||||
}
|
||||
|
||||
window.Open(pageTag);
|
||||
AppLogger.Info(
|
||||
"IndependentSettingsModule",
|
||||
$"WindowActivated; Source='{source}'; ReusedExisting={ReferenceEquals(_window, window)}; WasVisible={window.IsVisible}; PageTag='{pageTag ?? "<default>"}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("IndependentSettingsModule", $"Failed to open independent settings module window. Source='{source}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
ShowCore();
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(ShowCore, DispatcherPriority.Normal);
|
||||
}
|
||||
|
||||
public void CloseIfOpen()
|
||||
{
|
||||
void CloseCore()
|
||||
{
|
||||
if (_window is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_window.PrepareForForceClose();
|
||||
_window.Close();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("IndependentSettingsModule", "Failed to close independent settings module window during shutdown.", ex);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_window = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (Dispatcher.UIThread.CheckAccess())
|
||||
{
|
||||
CloseCore();
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.Post(CloseCore, DispatcherPriority.Send);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
internal sealed class SettingsCatalogService : ISettingsCatalog
|
||||
{
|
||||
private readonly List<SettingsSectionDefinition> _sections = [];
|
||||
private readonly object _gate = new();
|
||||
|
||||
public SettingsCatalogService()
|
||||
{
|
||||
// Built-in host sections for the next settings UI.
|
||||
_sections.AddRange(
|
||||
[
|
||||
new SettingsSectionDefinition("general", SettingsCategories.General, SettingsScope.App, "settings.general.title", iconKey: "Settings", sortOrder: 0),
|
||||
new SettingsSectionDefinition("appearance", SettingsCategories.Appearance, SettingsScope.App, "settings.appearance.title", iconKey: "DesignIdeas", sortOrder: 10),
|
||||
new SettingsSectionDefinition("components", SettingsCategories.Components, SettingsScope.ComponentInstance, "settings.components.title", iconKey: "GridDots", sortOrder: 20),
|
||||
new SettingsSectionDefinition("plugins", SettingsCategories.Plugins, SettingsScope.Plugin, "settings.plugins.title", iconKey: "PuzzlePiece", sortOrder: 30),
|
||||
new SettingsSectionDefinition("plugin-market", SettingsCategories.PluginMarket, SettingsScope.Plugin, "settings.plugin_market.title", iconKey: "Shop", sortOrder: 40),
|
||||
new SettingsSectionDefinition("update", SettingsCategories.Update, SettingsScope.App, "settings.update.title", iconKey: "ArrowSync", sortOrder: 50),
|
||||
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 60),
|
||||
new SettingsSectionDefinition("advanced", SettingsCategories.Advanced, SettingsScope.App, "settings.advanced.title", iconKey: "DeveloperBoard", sortOrder: 70)
|
||||
]);
|
||||
}
|
||||
|
||||
public IReadOnlyList<SettingsSectionDefinition> GetSections()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _sections
|
||||
.OrderBy(section => section.SortOrder)
|
||||
.ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<SettingsSectionDefinition> GetSections(SettingsScope scope)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
return _sections
|
||||
.Where(section => section.Scope == scope)
|
||||
.OrderBy(section => section.SortOrder)
|
||||
.ThenBy(section => section.Id, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
}
|
||||
}
|
||||
|
||||
public void RegisterPluginSections(string pluginId, IReadOnlyList<PluginSettingsSectionRegistration> sections)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(pluginId);
|
||||
var normalizedPluginId = pluginId.Trim();
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_sections.RemoveAll(section =>
|
||||
section.Scope == SettingsScope.Plugin &&
|
||||
string.Equals(section.SubjectId, normalizedPluginId, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
foreach (var registration in sections)
|
||||
{
|
||||
var definition = new SettingsSectionDefinition(
|
||||
id: $"{normalizedPluginId}:{registration.Id}",
|
||||
category: SettingsCategories.External,
|
||||
scope: SettingsScope.Plugin,
|
||||
titleLocalizationKey: registration.TitleLocalizationKey,
|
||||
descriptionLocalizationKey: registration.DescriptionLocalizationKey,
|
||||
iconKey: registration.IconKey,
|
||||
sortOrder: registration.SortOrder,
|
||||
subjectId: normalizedPluginId,
|
||||
options: registration.Options);
|
||||
_sections.Add(definition);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RemovePluginSections(string pluginId)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pluginId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_sections.RemoveAll(section =>
|
||||
section.Scope == SettingsScope.Plugin &&
|
||||
string.Equals(section.SubjectId, pluginId, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
}
|
||||
189
LanMountainDesktop/Services/Settings/SettingsContracts.cs
Normal file
189
LanMountainDesktop/Services/Settings/SettingsContracts.cs
Normal file
@@ -0,0 +1,189 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
public enum WallpaperMediaType
|
||||
{
|
||||
None,
|
||||
Image,
|
||||
Video
|
||||
}
|
||||
|
||||
public sealed record GridSettingsState(int ShortSideCells, string SpacingPreset, int EdgeInsetPercent);
|
||||
public sealed record WallpaperSettingsState(string? WallpaperPath, string Placement);
|
||||
public sealed record ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor);
|
||||
public sealed record StatusBarSettingsState(
|
||||
IReadOnlyList<string> TopStatusComponentIds,
|
||||
IReadOnlyList<string> PinnedTaskbarActions,
|
||||
bool EnableDynamicTaskbarActions,
|
||||
string TaskbarLayoutMode,
|
||||
string ClockDisplayFormat,
|
||||
string SpacingMode,
|
||||
int CustomSpacingPercent);
|
||||
public sealed record WeatherSettingsState(
|
||||
string LocationMode,
|
||||
string LocationKey,
|
||||
string LocationName,
|
||||
double Latitude,
|
||||
double Longitude,
|
||||
bool AutoRefreshLocation,
|
||||
string ExcludedAlerts,
|
||||
string IconPackId,
|
||||
bool NoTlsRequests,
|
||||
string LocationQuery);
|
||||
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
|
||||
public sealed record UpdateSettingsState(bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel);
|
||||
public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
|
||||
public sealed record PluginMarketPluginInfo(
|
||||
string Id,
|
||||
string Name,
|
||||
string Description,
|
||||
string Author,
|
||||
string Version,
|
||||
string ApiVersion,
|
||||
string MinHostVersion,
|
||||
string DownloadUrl,
|
||||
string ReleaseTag,
|
||||
string ReleaseAssetName,
|
||||
string IconUrl,
|
||||
string ReadmeUrl,
|
||||
string HomepageUrl,
|
||||
string RepositoryUrl,
|
||||
IReadOnlyList<string> Tags,
|
||||
DateTimeOffset PublishedAt,
|
||||
DateTimeOffset UpdatedAt);
|
||||
public sealed record PluginMarketIndexResult(
|
||||
bool Success,
|
||||
IReadOnlyList<PluginMarketPluginInfo> Plugins,
|
||||
string? Source,
|
||||
string? SourceLocation,
|
||||
string? WarningMessage,
|
||||
string? ErrorMessage);
|
||||
public sealed record PluginMarketInstallResult(
|
||||
bool Success,
|
||||
string? PluginId,
|
||||
string? PluginName,
|
||||
string? ErrorMessage);
|
||||
|
||||
public interface IGridSettingsService
|
||||
{
|
||||
GridSettingsState Get();
|
||||
void Save(GridSettingsState state);
|
||||
}
|
||||
|
||||
public interface IWallpaperSettingsService
|
||||
{
|
||||
WallpaperSettingsState Get();
|
||||
void Save(WallpaperSettingsState state);
|
||||
}
|
||||
|
||||
public interface IWallpaperMediaService
|
||||
{
|
||||
WallpaperMediaType DetectMediaType(string? path);
|
||||
Task<string?> ImportAssetAsync(string sourcePath, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IThemeAppearanceService
|
||||
{
|
||||
ThemeAppearanceSettingsState Get();
|
||||
void Save(ThemeAppearanceSettingsState state);
|
||||
MonetPalette BuildPalette(bool nightMode, string? wallpaperPath);
|
||||
}
|
||||
|
||||
public interface IStatusBarSettingsService
|
||||
{
|
||||
StatusBarSettingsState Get();
|
||||
void Save(StatusBarSettingsState state);
|
||||
}
|
||||
|
||||
public interface IWeatherProvider
|
||||
{
|
||||
Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
|
||||
string keyword,
|
||||
string? locale = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<WeatherQueryResult<WeatherSnapshot>> GetWeatherAsync(
|
||||
WeatherQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IWeatherSettingsService
|
||||
{
|
||||
WeatherSettingsState Get();
|
||||
void Save(WeatherSettingsState state);
|
||||
}
|
||||
|
||||
public interface IRegionSettingsService
|
||||
{
|
||||
RegionSettingsState Get();
|
||||
void Save(RegionSettingsState state);
|
||||
}
|
||||
|
||||
public interface IUpdateSettingsService
|
||||
{
|
||||
UpdateSettingsState Get();
|
||||
void Save(UpdateSettingsState state);
|
||||
Task<UpdateCheckResult> CheckForUpdatesAsync(Version currentVersion, bool includePrerelease, CancellationToken cancellationToken = default);
|
||||
Task<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface ILauncherCatalogService
|
||||
{
|
||||
StartMenuFolderNode LoadCatalog();
|
||||
}
|
||||
|
||||
public interface ILauncherPolicyService
|
||||
{
|
||||
LauncherSettingsSnapshot Get();
|
||||
void Save(LauncherSettingsSnapshot snapshot);
|
||||
}
|
||||
|
||||
public interface IPluginManagementSettingsService
|
||||
{
|
||||
PluginManagementSettingsState Get();
|
||||
void Save(PluginManagementSettingsState state);
|
||||
IReadOnlyList<InstalledPluginInfo> GetInstalledPlugins();
|
||||
bool SetPluginEnabled(string pluginId, bool isEnabled);
|
||||
bool DeleteInstalledPlugin(string pluginId);
|
||||
}
|
||||
|
||||
public interface IPluginMarketSettingsService
|
||||
{
|
||||
Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default);
|
||||
Task<PluginMarketInstallResult> InstallAsync(string pluginId, CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
public interface IApplicationInfoService
|
||||
{
|
||||
string GetAppVersionText();
|
||||
AppRenderBackendInfo GetRenderBackendInfo();
|
||||
}
|
||||
|
||||
public interface ISettingsFacadeService
|
||||
{
|
||||
ISettingsService Settings { get; }
|
||||
ISettingsCatalog Catalog { get; }
|
||||
IGridSettingsService Grid { get; }
|
||||
IWallpaperSettingsService Wallpaper { get; }
|
||||
IWallpaperMediaService WallpaperMedia { get; }
|
||||
IThemeAppearanceService Theme { get; }
|
||||
IStatusBarSettingsService StatusBar { get; }
|
||||
IWeatherSettingsService Weather { get; }
|
||||
IRegionSettingsService Region { get; }
|
||||
IUpdateSettingsService Update { get; }
|
||||
ILauncherCatalogService LauncherCatalog { get; }
|
||||
ILauncherPolicyService LauncherPolicy { get; }
|
||||
IPluginManagementSettingsService PluginManagement { get; }
|
||||
IPluginMarketSettingsService PluginMarket { get; }
|
||||
IApplicationInfoService ApplicationInfo { get; }
|
||||
}
|
||||
627
LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
Normal file
627
LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
Normal file
@@ -0,0 +1,627 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia.Media.Imaging;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.PluginMarket;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
internal sealed class GridSettingsService : IGridSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
|
||||
public GridSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new GridSettingsState(
|
||||
snapshot.GridShortSideCells,
|
||||
snapshot.GridSpacingPreset,
|
||||
snapshot.DesktopEdgeInsetPercent);
|
||||
}
|
||||
|
||||
public void Save(GridSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.GridShortSideCells = state.ShortSideCells;
|
||||
snapshot.GridSpacingPreset = state.SpacingPreset;
|
||||
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WallpaperSettingsService : IWallpaperSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
|
||||
public WallpaperSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new WallpaperSettingsState(snapshot.WallpaperPath, snapshot.WallpaperPlacement);
|
||||
}
|
||||
|
||||
public void Save(WallpaperSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.WallpaperPath = state.WallpaperPath;
|
||||
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
|
||||
? "Fill"
|
||||
: state.Placement.Trim();
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WallpaperMediaService : IWallpaperMediaService
|
||||
{
|
||||
private static readonly HashSet<string> ImageExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp"
|
||||
};
|
||||
|
||||
private static readonly HashSet<string> VideoExtensions = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
".mp4", ".mkv", ".webm", ".avi", ".mov", ".m4v"
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
if (VideoExtensions.Contains(extension))
|
||||
{
|
||||
return WallpaperMediaType.Video;
|
||||
}
|
||||
|
||||
return WallpaperMediaType.None;
|
||||
}
|
||||
|
||||
public async Task<string?> 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 AppSettingsService _appSettingsService = new();
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly WallpaperMediaService _wallpaperMediaService = new();
|
||||
|
||||
public ThemeAppearanceSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new ThemeAppearanceSettingsState(
|
||||
snapshot.IsNightMode ?? false,
|
||||
snapshot.ThemeColor);
|
||||
}
|
||||
|
||||
public void Save(ThemeAppearanceSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.IsNightMode = state.IsNightMode;
|
||||
snapshot.ThemeColor = state.ThemeColor;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath)
|
||||
{
|
||||
Bitmap? bitmap = null;
|
||||
|
||||
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);
|
||||
}
|
||||
finally
|
||||
{
|
||||
bitmap?.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class StatusBarSettingsService : IStatusBarSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
|
||||
public StatusBarSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new StatusBarSettingsState(
|
||||
snapshot.TopStatusComponentIds?.ToArray() ?? [],
|
||||
snapshot.PinnedTaskbarActions?.ToArray() ?? [],
|
||||
snapshot.EnableDynamicTaskbarActions,
|
||||
snapshot.TaskbarLayoutMode,
|
||||
snapshot.ClockDisplayFormat,
|
||||
snapshot.StatusBarSpacingMode,
|
||||
snapshot.StatusBarCustomSpacingPercent);
|
||||
}
|
||||
|
||||
public void Save(StatusBarSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? [];
|
||||
snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? [];
|
||||
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
|
||||
snapshot.TaskbarLayoutMode = state.TaskbarLayoutMode;
|
||||
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
|
||||
snapshot.StatusBarSpacingMode = state.SpacingMode;
|
||||
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WeatherProviderAdapter : IWeatherProvider
|
||||
{
|
||||
private readonly IWeatherDataService _weatherDataService = new XiaomiWeatherService();
|
||||
|
||||
public Task<WeatherQueryResult<IReadOnlyList<WeatherLocation>>> SearchLocationsAsync(
|
||||
string keyword,
|
||||
string? locale = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _weatherDataService.SearchLocationsAsync(keyword, locale, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<WeatherQueryResult<WeatherSnapshot>> GetWeatherAsync(
|
||||
WeatherQuery query,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _weatherDataService.GetWeatherAsync(query, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class WeatherSettingsService : IWeatherSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
|
||||
public WeatherSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new WeatherSettingsState(
|
||||
snapshot.WeatherLocationMode,
|
||||
snapshot.WeatherLocationKey,
|
||||
snapshot.WeatherLocationName,
|
||||
snapshot.WeatherLatitude,
|
||||
snapshot.WeatherLongitude,
|
||||
snapshot.WeatherAutoRefreshLocation,
|
||||
snapshot.WeatherExcludedAlerts,
|
||||
snapshot.WeatherIconPackId,
|
||||
snapshot.WeatherNoTlsRequests,
|
||||
snapshot.WeatherLocationQuery);
|
||||
}
|
||||
|
||||
public void Save(WeatherSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.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 = state.IconPackId;
|
||||
snapshot.WeatherNoTlsRequests = state.NoTlsRequests;
|
||||
snapshot.WeatherLocationQuery = state.LocationQuery;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class RegionSettingsService : IRegionSettingsService
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
|
||||
public RegionSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId);
|
||||
}
|
||||
|
||||
public void Save(RegionSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.LanguageCode = string.IsNullOrWhiteSpace(state.LanguageCode)
|
||||
? "zh-CN"
|
||||
: state.LanguageCode.Trim();
|
||||
snapshot.TimeZoneId = string.IsNullOrWhiteSpace(state.TimeZoneId)
|
||||
? null
|
||||
: state.TimeZoneId.Trim();
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposable
|
||||
{
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
|
||||
|
||||
public UpdateSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new UpdateSettingsState(
|
||||
snapshot.AutoCheckUpdates,
|
||||
snapshot.IncludePrereleaseUpdates,
|
||||
snapshot.UpdateChannel);
|
||||
}
|
||||
|
||||
public void Save(UpdateSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
|
||||
snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates;
|
||||
snapshot.UpdateChannel = state.UpdateChannel;
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
public Task<UpdateCheckResult> CheckForUpdatesAsync(
|
||||
Version currentVersion,
|
||||
bool includePrerelease,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _releaseUpdateService.CheckForUpdatesAsync(currentVersion, includePrerelease, cancellationToken);
|
||||
}
|
||||
|
||||
public Task<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
return _releaseUpdateService.DownloadAssetAsync(asset, destinationFilePath, progress, cancellationToken);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_releaseUpdateService.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
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 AppSettingsService _appSettingsService = new();
|
||||
private readonly PluginRuntimeService? _pluginRuntimeService;
|
||||
|
||||
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
|
||||
{
|
||||
_pluginRuntimeService = pluginRuntimeService;
|
||||
}
|
||||
|
||||
public PluginManagementSettingsState Get()
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []);
|
||||
}
|
||||
|
||||
public void Save(PluginManagementSettingsState state)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? [];
|
||||
_appSettingsService.Save(snapshot);
|
||||
}
|
||||
|
||||
public IReadOnlyList<InstalledPluginInfo> 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 PluginMarketSettingsService : IPluginMarketSettingsService, IDisposable
|
||||
{
|
||||
private readonly PluginRuntimeService? _pluginRuntimeService;
|
||||
private readonly AirAppMarketIndexService _indexService;
|
||||
private readonly AirAppMarketInstallService? _installService;
|
||||
private readonly Dictionary<string, AirAppMarketPluginEntry> _cachedPlugins = new(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
public PluginMarketSettingsService(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 async Task<PluginMarketIndexResult> LoadIndexAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
var result = await _indexService.LoadAsync(cancellationToken);
|
||||
if (!result.Success || result.Document is null)
|
||||
{
|
||||
return new PluginMarketIndexResult(
|
||||
false,
|
||||
[],
|
||||
result.Source?.ToString(),
|
||||
result.SourceLocation,
|
||||
result.WarningMessage,
|
||||
result.ErrorMessage);
|
||||
}
|
||||
|
||||
_cachedPlugins.Clear();
|
||||
var plugins = result.Document.Plugins
|
||||
.Select(entry =>
|
||||
{
|
||||
_cachedPlugins[entry.Id] = entry;
|
||||
return new PluginMarketPluginInfo(
|
||||
entry.Id,
|
||||
entry.Name,
|
||||
entry.Description,
|
||||
entry.Author,
|
||||
entry.Version,
|
||||
entry.ApiVersion,
|
||||
entry.MinHostVersion,
|
||||
entry.DownloadUrl,
|
||||
entry.ReleaseTag,
|
||||
entry.ReleaseAssetName,
|
||||
entry.IconUrl,
|
||||
entry.ReadmeUrl,
|
||||
entry.HomepageUrl,
|
||||
entry.RepositoryUrl,
|
||||
entry.Tags,
|
||||
entry.PublishedAt,
|
||||
entry.UpdatedAt);
|
||||
})
|
||||
.ToArray();
|
||||
|
||||
return new PluginMarketIndexResult(
|
||||
true,
|
||||
plugins,
|
||||
result.Source?.ToString(),
|
||||
result.SourceLocation,
|
||||
result.WarningMessage,
|
||||
null);
|
||||
}
|
||||
|
||||
public async Task<PluginMarketInstallResult> InstallAsync(
|
||||
string pluginId,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(pluginId))
|
||||
{
|
||||
return new PluginMarketInstallResult(false, null, null, "Plugin id is required.");
|
||||
}
|
||||
|
||||
if (_installService is null || _pluginRuntimeService is null)
|
||||
{
|
||||
return new PluginMarketInstallResult(
|
||||
false,
|
||||
pluginId,
|
||||
null,
|
||||
"Plugin runtime is unavailable.");
|
||||
}
|
||||
|
||||
if (!_cachedPlugins.TryGetValue(pluginId, out var entry))
|
||||
{
|
||||
var load = await LoadIndexAsync(cancellationToken);
|
||||
if (!load.Success)
|
||||
{
|
||||
return new PluginMarketInstallResult(false, pluginId, null, load.ErrorMessage);
|
||||
}
|
||||
|
||||
if (!_cachedPlugins.TryGetValue(pluginId, out entry))
|
||||
{
|
||||
return new PluginMarketInstallResult(false, pluginId, null, "Plugin was not found in market index.");
|
||||
}
|
||||
}
|
||||
|
||||
var result = await _installService.InstallAsync(entry, cancellationToken);
|
||||
if (!result.Success)
|
||||
{
|
||||
return new PluginMarketInstallResult(false, entry.Id, entry.Name, result.ErrorMessage);
|
||||
}
|
||||
|
||||
return new PluginMarketInstallResult(true, result.Manifest?.Id ?? entry.Id, result.Manifest?.Name ?? entry.Name, null);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_indexService.Dispose();
|
||||
_installService?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class ApplicationInfoService : IApplicationInfoService
|
||||
{
|
||||
public string GetAppVersionText()
|
||||
{
|
||||
var version = typeof(App).Assembly.GetName().Version;
|
||||
return version is null
|
||||
? "0.0.0"
|
||||
: new Version(
|
||||
Math.Max(0, version.Major),
|
||||
Math.Max(0, version.Minor),
|
||||
Math.Max(0, version.Build)).ToString(3);
|
||||
}
|
||||
|
||||
public AppRenderBackendInfo GetRenderBackendInfo()
|
||||
{
|
||||
return AppRenderBackendDiagnostics.Detect();
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposable
|
||||
{
|
||||
private readonly UpdateSettingsService _updateSettingsService;
|
||||
private readonly PluginMarketSettingsService _pluginMarketSettingsService;
|
||||
|
||||
public SettingsFacadeService(PluginRuntimeService? pluginRuntimeService = null)
|
||||
{
|
||||
Settings = new SettingsService();
|
||||
Catalog = new SettingsCatalogService();
|
||||
Grid = new GridSettingsService();
|
||||
Wallpaper = new WallpaperSettingsService();
|
||||
WallpaperMedia = new WallpaperMediaService();
|
||||
Theme = new ThemeAppearanceService();
|
||||
StatusBar = new StatusBarSettingsService();
|
||||
Weather = new WeatherSettingsService();
|
||||
Region = new RegionSettingsService();
|
||||
_updateSettingsService = new UpdateSettingsService();
|
||||
Update = _updateSettingsService;
|
||||
LauncherCatalog = new LauncherCatalogService();
|
||||
LauncherPolicy = new LauncherPolicyService();
|
||||
PluginManagement = new PluginManagementSettingsService(pluginRuntimeService);
|
||||
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
|
||||
PluginMarket = _pluginMarketSettingsService;
|
||||
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 IWeatherSettingsService Weather { get; }
|
||||
|
||||
public IRegionSettingsService Region { get; }
|
||||
|
||||
public IUpdateSettingsService Update { get; }
|
||||
|
||||
public ILauncherCatalogService LauncherCatalog { get; }
|
||||
|
||||
public ILauncherPolicyService LauncherPolicy { get; }
|
||||
|
||||
public IPluginManagementSettingsService PluginManagement { get; }
|
||||
|
||||
public IPluginMarketSettingsService PluginMarket { get; }
|
||||
|
||||
public IApplicationInfoService ApplicationInfo { get; }
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_updateSettingsService.Dispose();
|
||||
_pluginMarketSettingsService.Dispose();
|
||||
}
|
||||
}
|
||||
423
LanMountainDesktop/Services/Settings/SettingsService.cs
Normal file
423
LanMountainDesktop/Services/Settings/SettingsService.cs
Normal file
@@ -0,0 +1,423 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text.Json;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
|
||||
namespace LanMountainDesktop.Services.Settings;
|
||||
|
||||
internal sealed class SettingsService : ISettingsService
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
PropertyNameCaseInsensitive = true,
|
||||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||||
WriteIndented = true
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly LauncherSettingsService _launcherSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private readonly string _pluginSettingsPath;
|
||||
private readonly object _pluginSettingsGate = new();
|
||||
|
||||
public SettingsService()
|
||||
{
|
||||
var root = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
||||
"LanMountainDesktop");
|
||||
_pluginSettingsPath = Path.Combine(root, "plugin-settings.json");
|
||||
}
|
||||
|
||||
public event EventHandler<SettingsChangedEvent>? Changed;
|
||||
|
||||
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
|
||||
{
|
||||
return scope switch
|
||||
{
|
||||
SettingsScope.App => ConvertSnapshot<AppSettingsSnapshot, T>(_appSettingsService.Load()),
|
||||
SettingsScope.Launcher => ConvertSnapshot<LauncherSettingsSnapshot, T>(_launcherSettingsService.Load()),
|
||||
SettingsScope.ComponentInstance => LoadComponentSnapshot<T>(subjectId, placementId),
|
||||
SettingsScope.Plugin => LoadSection<T>(scope, EnsureKey(subjectId), sectionId: "__snapshot__", placementId),
|
||||
_ => new T()
|
||||
};
|
||||
}
|
||||
|
||||
public void SaveSnapshot<T>(
|
||||
SettingsScope scope,
|
||||
T snapshot,
|
||||
string? subjectId = null,
|
||||
string? placementId = null,
|
||||
string? sectionId = null,
|
||||
IReadOnlyCollection<string>? changedKeys = null)
|
||||
{
|
||||
switch (scope)
|
||||
{
|
||||
case SettingsScope.App:
|
||||
_appSettingsService.Save(ConvertSnapshot<T, AppSettingsSnapshot>(snapshot));
|
||||
break;
|
||||
case SettingsScope.Launcher:
|
||||
_launcherSettingsService.Save(ConvertSnapshot<T, LauncherSettingsSnapshot>(snapshot));
|
||||
break;
|
||||
case SettingsScope.ComponentInstance:
|
||||
SaveComponentSnapshot(subjectId, placementId, snapshot);
|
||||
break;
|
||||
case SettingsScope.Plugin:
|
||||
SaveSection(scope, EnsureKey(subjectId), "__snapshot__", snapshot, placementId, changedKeys);
|
||||
break;
|
||||
}
|
||||
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
|
||||
}
|
||||
|
||||
public T LoadSection<T>(
|
||||
SettingsScope scope,
|
||||
string subjectId,
|
||||
string sectionId,
|
||||
string? placementId = null) where T : new()
|
||||
{
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
return _componentSettingsService.LoadPluginSettings<T>(EnsureKey(subjectId), placementId);
|
||||
}
|
||||
|
||||
if (scope != SettingsScope.Plugin)
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
|
||||
lock (_pluginSettingsGate)
|
||||
{
|
||||
var document = LoadPluginDocumentLocked();
|
||||
if (!document.Sections.TryGetValue(EnsureKey(subjectId), out var pluginSections) ||
|
||||
!pluginSections.TryGetValue(EnsureKey(sectionId), out var payload))
|
||||
{
|
||||
return new T();
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<T>(payload.GetRawText(), SerializerOptions) ?? new T();
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveSection<T>(
|
||||
SettingsScope scope,
|
||||
string subjectId,
|
||||
string sectionId,
|
||||
T section,
|
||||
string? placementId = null,
|
||||
IReadOnlyCollection<string>? changedKeys = null)
|
||||
{
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, section);
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope != SettingsScope.Plugin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_pluginSettingsGate)
|
||||
{
|
||||
var document = LoadPluginDocumentLocked();
|
||||
var pluginId = EnsureKey(subjectId);
|
||||
if (!document.Sections.TryGetValue(pluginId, out var pluginSections))
|
||||
{
|
||||
pluginSections = new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
|
||||
document.Sections[pluginId] = pluginSections;
|
||||
}
|
||||
|
||||
pluginSections[EnsureKey(sectionId)] = JsonSerializer.SerializeToElement(section, SerializerOptions).Clone();
|
||||
PersistPluginDocumentLocked(document);
|
||||
}
|
||||
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
|
||||
}
|
||||
|
||||
public void DeleteSection(SettingsScope scope, string subjectId, string sectionId, string? placementId = null)
|
||||
{
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
_componentSettingsService.DeletePluginSettings(EnsureKey(subjectId), placementId);
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope != SettingsScope.Plugin)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_pluginSettingsGate)
|
||||
{
|
||||
var document = LoadPluginDocumentLocked();
|
||||
var pluginId = EnsureKey(subjectId);
|
||||
if (document.Sections.TryGetValue(pluginId, out var sections) &&
|
||||
sections.Remove(EnsureKey(sectionId)))
|
||||
{
|
||||
if (sections.Count == 0)
|
||||
{
|
||||
document.Sections.Remove(pluginId);
|
||||
}
|
||||
|
||||
PersistPluginDocumentLocked(document);
|
||||
}
|
||||
}
|
||||
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId));
|
||||
}
|
||||
|
||||
public T? GetValue<T>(
|
||||
SettingsScope scope,
|
||||
string key,
|
||||
string? subjectId = null,
|
||||
string? placementId = null,
|
||||
string? sectionId = null)
|
||||
{
|
||||
var snapshot = scope switch
|
||||
{
|
||||
SettingsScope.App => JsonSerializer.SerializeToElement(_appSettingsService.Load(), SerializerOptions),
|
||||
SettingsScope.Launcher => JsonSerializer.SerializeToElement(_launcherSettingsService.Load(), SerializerOptions),
|
||||
SettingsScope.ComponentInstance => JsonSerializer.SerializeToElement(
|
||||
_componentSettingsService.LoadForComponent(EnsureKey(subjectId), placementId),
|
||||
SerializerOptions),
|
||||
SettingsScope.Plugin => JsonSerializer.SerializeToElement(
|
||||
LoadSection<Dictionary<string, JsonElement>>(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", placementId),
|
||||
SerializerOptions),
|
||||
_ => default
|
||||
};
|
||||
|
||||
if (snapshot.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
foreach (var property in snapshot.EnumerateObject())
|
||||
{
|
||||
if (!string.Equals(property.Name, key, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return property.Value.Deserialize<T>(SerializerOptions);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
return default;
|
||||
}
|
||||
|
||||
public void SetValue<T>(
|
||||
SettingsScope scope,
|
||||
string key,
|
||||
T value,
|
||||
string? subjectId = null,
|
||||
string? placementId = null,
|
||||
string? sectionId = null,
|
||||
IReadOnlyCollection<string>? changedKeys = null)
|
||||
{
|
||||
if (scope == SettingsScope.Plugin)
|
||||
{
|
||||
var dict = LoadSection<Dictionary<string, JsonElement>>(
|
||||
SettingsScope.Plugin,
|
||||
EnsureKey(subjectId),
|
||||
sectionId ?? "__root__",
|
||||
placementId);
|
||||
dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
|
||||
SaveSection(SettingsScope.Plugin, EnsureKey(subjectId), sectionId ?? "__root__", dict, placementId, changedKeys ?? [key]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope == SettingsScope.ComponentInstance)
|
||||
{
|
||||
var dict = _componentSettingsService.LoadPluginSettings<Dictionary<string, JsonElement>>(EnsureKey(subjectId), placementId);
|
||||
dict[key] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
|
||||
_componentSettingsService.SavePluginSettings(EnsureKey(subjectId), placementId, dict);
|
||||
OnChanged(new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys ?? [key]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope == SettingsScope.App)
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
var updated = UpdateObjectKey(snapshot, key, value);
|
||||
_appSettingsService.Save(updated);
|
||||
OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key]));
|
||||
return;
|
||||
}
|
||||
|
||||
if (scope == SettingsScope.Launcher)
|
||||
{
|
||||
var snapshot = _launcherSettingsService.Load();
|
||||
var updated = UpdateObjectKey(snapshot, key, value);
|
||||
_launcherSettingsService.Save(updated);
|
||||
OnChanged(new SettingsChangedEvent(scope, null, null, sectionId, changedKeys ?? [key]));
|
||||
}
|
||||
}
|
||||
|
||||
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
|
||||
{
|
||||
return new ComponentSettingsAccessor(this, componentId, placementId);
|
||||
}
|
||||
|
||||
private T LoadComponentSnapshot<T>(string? componentId, string? placementId) where T : new()
|
||||
{
|
||||
var snapshot = _componentSettingsService.LoadForComponent(EnsureKey(componentId), placementId);
|
||||
return ConvertSnapshot<ComponentSettingsSnapshot, T>(snapshot);
|
||||
}
|
||||
|
||||
private void SaveComponentSnapshot<T>(string? componentId, string? placementId, T snapshot)
|
||||
{
|
||||
var converted = ConvertSnapshot<T, ComponentSettingsSnapshot>(snapshot);
|
||||
_componentSettingsService.SaveForComponent(EnsureKey(componentId), placementId, converted);
|
||||
}
|
||||
|
||||
private static TOut ConvertSnapshot<TIn, TOut>(TIn source) where TOut : new()
|
||||
{
|
||||
if (source is null)
|
||||
{
|
||||
return new TOut();
|
||||
}
|
||||
|
||||
if (source is TOut direct)
|
||||
{
|
||||
return direct;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(source, SerializerOptions);
|
||||
return JsonSerializer.Deserialize<TOut>(json, SerializerOptions) ?? new TOut();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return new TOut();
|
||||
}
|
||||
}
|
||||
|
||||
private static TSnapshot UpdateObjectKey<TSnapshot, TValue>(TSnapshot snapshot, string key, TValue value)
|
||||
where TSnapshot : new()
|
||||
{
|
||||
var bag = JsonSerializer.Deserialize<Dictionary<string, JsonElement>>(
|
||||
JsonSerializer.Serialize(snapshot, SerializerOptions),
|
||||
SerializerOptions) ?? new Dictionary<string, JsonElement>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
var actualKey = bag.Keys.FirstOrDefault(existing => string.Equals(existing, key, StringComparison.OrdinalIgnoreCase)) ?? key;
|
||||
bag[actualKey] = JsonSerializer.SerializeToElement(value, SerializerOptions).Clone();
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonSerializer.Serialize(bag, SerializerOptions);
|
||||
return JsonSerializer.Deserialize<TSnapshot>(json, SerializerOptions) ?? new TSnapshot();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return snapshot is null ? new TSnapshot() : snapshot;
|
||||
}
|
||||
}
|
||||
|
||||
private PluginSettingsDocument LoadPluginDocumentLocked()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!File.Exists(_pluginSettingsPath))
|
||||
{
|
||||
return new PluginSettingsDocument();
|
||||
}
|
||||
|
||||
var json = File.ReadAllText(_pluginSettingsPath);
|
||||
return JsonSerializer.Deserialize<PluginSettingsDocument>(json, SerializerOptions) ?? new PluginSettingsDocument();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("SettingsService", $"Failed to load plugin settings '{_pluginSettingsPath}'.", ex);
|
||||
return new PluginSettingsDocument();
|
||||
}
|
||||
}
|
||||
|
||||
private void PersistPluginDocumentLocked(PluginSettingsDocument document)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(_pluginSettingsPath);
|
||||
if (!string.IsNullOrWhiteSpace(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(_pluginSettingsPath, JsonSerializer.Serialize(document, SerializerOptions));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
AppLogger.Warn("SettingsService", $"Failed to persist plugin settings '{_pluginSettingsPath}'.", ex);
|
||||
}
|
||||
}
|
||||
|
||||
private static string EnsureKey(string? value)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(value) ? "__default__" : value.Trim();
|
||||
}
|
||||
|
||||
private void OnChanged(SettingsChangedEvent e)
|
||||
{
|
||||
try
|
||||
{
|
||||
Changed?.Invoke(this, e);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Never let a subscriber break settings persistence.
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ComponentSettingsAccessor : IComponentSettingsAccessor
|
||||
{
|
||||
private readonly SettingsService _settingsService;
|
||||
|
||||
public ComponentSettingsAccessor(SettingsService settingsService, string componentId, string? placementId)
|
||||
{
|
||||
_settingsService = settingsService;
|
||||
ComponentId = componentId;
|
||||
PlacementId = placementId;
|
||||
}
|
||||
|
||||
public string ComponentId { get; }
|
||||
|
||||
public string? PlacementId { get; }
|
||||
|
||||
public T LoadSnapshot<T>() where T : new()
|
||||
=> _settingsService.LoadSnapshot<T>(SettingsScope.ComponentInstance, ComponentId, PlacementId);
|
||||
|
||||
public void SaveSnapshot<T>(T snapshot, IReadOnlyCollection<string>? changedKeys = null)
|
||||
=> _settingsService.SaveSnapshot(SettingsScope.ComponentInstance, snapshot, ComponentId, PlacementId, changedKeys: changedKeys);
|
||||
|
||||
public T LoadSection<T>(string sectionId) where T : new()
|
||||
=> _settingsService.LoadSection<T>(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
|
||||
|
||||
public void SaveSection<T>(string sectionId, T section, IReadOnlyCollection<string>? changedKeys = null)
|
||||
=> _settingsService.SaveSection(SettingsScope.ComponentInstance, ComponentId, sectionId, section, PlacementId, changedKeys);
|
||||
|
||||
public void DeleteSection(string sectionId)
|
||||
=> _settingsService.DeleteSection(SettingsScope.ComponentInstance, ComponentId, sectionId, PlacementId);
|
||||
|
||||
public T? GetValue<T>(string key)
|
||||
=> _settingsService.GetValue<T>(SettingsScope.ComponentInstance, key, ComponentId, PlacementId);
|
||||
|
||||
public void SetValue<T>(string key, T value, IReadOnlyCollection<string>? changedKeys = null)
|
||||
=> _settingsService.SetValue(SettingsScope.ComponentInstance, key, value, ComponentId, PlacementId, changedKeys: changedKeys);
|
||||
}
|
||||
|
||||
private sealed class PluginSettingsDocument
|
||||
{
|
||||
public Dictionary<string, Dictionary<string, JsonElement>> Sections { get; set; } = new(StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user