settings_re4

This commit is contained in:
lincube
2026-03-13 22:20:12 +08:00
parent 3b3f060f33
commit 5fdaa2539b
89 changed files with 5778 additions and 192 deletions

View File

@@ -6,29 +6,20 @@ using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Views;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
public sealed record ComponentLibraryCreateContext(
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
string? PlacementId = null);
public interface IComponentLibraryService
public interface IEmbeddedComponentLibraryService
{
IReadOnlyList<DesktopComponentDefinition> GetDefinitions();
void Open(MainWindow window);
bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception);
void Close(MainWindow window);
void Toggle(MainWindow window);
}
public interface IComponentLibraryWindowService
public interface IDetachedComponentLibraryWindowService
{
void Open(MainWindow window);
@@ -53,6 +44,31 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
return _registry.GetAll().ToArray();
}
public IReadOnlyList<ComponentLibraryCategoryEntry> GetDesktopCategories()
{
return _runtimeRegistry
.GetDesktopComponents()
.GroupBy(
descriptor => string.IsNullOrWhiteSpace(descriptor.Definition.Category)
? "Other"
: descriptor.Definition.Category.Trim(),
StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
.Select(group => new ComponentLibraryCategoryEntry(
group.Key,
group
.OrderBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.Select(descriptor => new ComponentLibraryComponentEntry(
descriptor.Definition.Id,
descriptor.Definition.DisplayName,
descriptor.DisplayNameLocalizationKey,
group.Key,
descriptor.Definition.MinWidthCells,
descriptor.Definition.MinHeightCells))
.ToArray()))
.ToArray();
}
public bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
@@ -75,6 +91,7 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
context.WeatherInfoService,
context.RecommendationInfoService,
context.CalculatorDataService,
context.SettingsFacade,
context.PlacementId);
return true;
}
@@ -86,7 +103,7 @@ internal sealed class ComponentLibraryService : IComponentLibraryService
}
}
internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowService
internal sealed class EmbeddedComponentLibraryService : IEmbeddedComponentLibraryService
{
public void Open(MainWindow window)
{
@@ -112,3 +129,30 @@ internal sealed class ComponentLibraryWindowService : IComponentLibraryWindowSer
window.OpenComponentLibraryWindowFromService();
}
}
internal sealed class DetachedComponentLibraryWindowService : IDetachedComponentLibraryWindowService
{
public void Open(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.OpenDetachedComponentLibraryWindowFromService();
}
public void Close(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
window.CloseDetachedComponentLibraryWindowFromService();
}
public void Toggle(MainWindow window)
{
ArgumentNullException.ThrowIfNull(window);
if (window.IsDetachedComponentLibraryWindowOpenFromService)
{
window.CloseDetachedComponentLibraryWindowFromService();
return;
}
window.OpenDetachedComponentLibraryWindowFromService();
}
}

View File

@@ -1,6 +1,7 @@
using System;
using System.Reflection;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
@@ -19,6 +20,11 @@ public sealed class ComponentSettingsService : IComponentInstanceSettingsStore
_settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
}
public ComponentSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
internal ComponentSettingsService(string settingsDirectory)
{
if (string.IsNullOrWhiteSpace(settingsDirectory))

View File

@@ -33,7 +33,8 @@ public static class DesktopComponentRegistryFactory
public static DesktopComponentRuntimeRegistry CreateRuntimeRegistry(
ComponentRegistry componentRegistry,
PluginRuntimeService? pluginRuntimeService)
PluginRuntimeService? pluginRuntimeService,
ISettingsFacadeService settingsFacade)
{
var registrations = DesktopComponentRuntimeRegistry.GetDefaultRegistrations().ToList();
var registeredIds = new HashSet<string>(
@@ -65,6 +66,7 @@ public static class DesktopComponentRegistryFactory
}
}
_ = settingsFacade;
return new DesktopComponentRuntimeRegistry(componentRegistry, registrations);
}
@@ -116,7 +118,7 @@ public static class DesktopComponentRegistryFactory
try
{
var settingsService = contribution.Plugin.Services.GetService(typeof(ISettingsService)) as ISettingsService
?? HostSettingsFacadeProvider.GetOrCreate().Settings;
?? context.SettingsService;
var pluginSettings = new PluginScopedSettingsService(
contribution.Plugin.Manifest.Id,
settingsService);

View File

@@ -0,0 +1,14 @@
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
internal static class HostComponentSettingsStoreProvider
{
private static readonly IComponentInstanceSettingsStore Instance =
new ComponentSettingsService(HostSettingsFacadeProvider.GetOrCreate().Settings);
public static IComponentInstanceSettingsStore GetOrCreate()
{
return Instance;
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Services.Settings;
namespace LanMountainDesktop.Services;
public sealed record ComponentLibraryComponentEntry(
string ComponentId,
string DisplayName,
string? DisplayNameLocalizationKey,
string CategoryId,
int MinWidthCells,
int MinHeightCells);
public sealed record ComponentLibraryCategoryEntry(
string Id,
IReadOnlyList<ComponentLibraryComponentEntry> Components);
public sealed record ComponentLibraryCreateContext(
double CellSize,
TimeZoneService TimeZoneService,
IWeatherInfoService WeatherInfoService,
IRecommendationInfoService RecommendationInfoService,
ICalculatorDataService CalculatorDataService,
ISettingsFacadeService SettingsFacade,
string? PlacementId = null);
public interface IComponentLibraryService
{
IReadOnlyList<DesktopComponentDefinition> GetDefinitions();
IReadOnlyList<ComponentLibraryCategoryEntry> GetDesktopCategories();
bool TryCreateControl(
string componentId,
ComponentLibraryCreateContext context,
out Control? control,
out Exception? exception);
}

View File

@@ -19,10 +19,7 @@ internal sealed class SettingsCatalogService : ISettingsCatalog
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)
new SettingsSectionDefinition("about", SettingsCategories.About, SettingsScope.App, "settings.about.title", iconKey: "Info", sortOrder: 40)
]);
}

View File

@@ -16,7 +16,7 @@ public enum WallpaperMediaType
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 ThemeAppearanceSettingsState(bool IsNightMode, string? ThemeColor, bool UseSystemChrome);
public sealed record StatusBarSettingsState(
IReadOnlyList<string> TopStatusComponentIds,
IReadOnlyList<string> PinnedTaskbarActions,

View File

@@ -7,18 +7,24 @@ using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
namespace LanMountainDesktop.Services.Settings;
internal sealed class GridSettingsService : IGridSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
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 = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new GridSettingsState(
snapshot.GridShortSideCells,
snapshot.GridSpacingPreset,
@@ -27,11 +33,19 @@ internal sealed class GridSettingsService : IGridSettingsService
public void Save(GridSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.GridShortSideCells = state.ShortSideCells;
snapshot.GridSpacingPreset = state.SpacingPreset;
snapshot.DesktopEdgeInsetPercent = state.EdgeInsetPercent;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.GridShortSideCells),
nameof(AppSettingsSnapshot.GridSpacingPreset),
nameof(AppSettingsSnapshot.DesktopEdgeInsetPercent)
]);
}
public string NormalizeSpacingPreset(string? value)
@@ -67,22 +81,34 @@ internal sealed class GridSettingsService : IGridSettingsService
internal sealed class WallpaperSettingsService : IWallpaperSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
public WallpaperSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public WallpaperSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new WallpaperSettingsState(snapshot.WallpaperPath, snapshot.WallpaperPlacement);
}
public void Save(WallpaperSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.WallpaperPath = state.WallpaperPath;
snapshot.WallpaperPlacement = string.IsNullOrWhiteSpace(state.Placement)
? "Fill"
: state.Placement.Trim();
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.WallpaperPath),
nameof(AppSettingsSnapshot.WallpaperPlacement)
]);
}
}
@@ -182,24 +208,39 @@ internal sealed class WallpaperMediaService : IWallpaperMediaService
internal sealed class ThemeAppearanceService : IThemeAppearanceService
{
private readonly AppSettingsService _appSettingsService = new();
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 = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new ThemeAppearanceSettingsState(
snapshot.IsNightMode ?? false,
snapshot.ThemeColor);
snapshot.ThemeColor,
snapshot.UseSystemChrome);
}
public void Save(ThemeAppearanceSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.IsNightMode = state.IsNightMode;
snapshot.ThemeColor = state.ThemeColor;
_appSettingsService.Save(snapshot);
snapshot.UseSystemChrome = state.UseSystemChrome;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.IsNightMode),
nameof(AppSettingsSnapshot.ThemeColor),
nameof(AppSettingsSnapshot.UseSystemChrome)
]);
}
public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath)
@@ -236,11 +277,16 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
internal sealed class StatusBarSettingsService : IStatusBarSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
public StatusBarSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public StatusBarSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new StatusBarSettingsState(
snapshot.TopStatusComponentIds?.ToArray() ?? [],
snapshot.PinnedTaskbarActions?.ToArray() ?? [],
@@ -253,7 +299,7 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
public void Save(StatusBarSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.TopStatusComponentIds = state.TopStatusComponentIds?.ToList() ?? [];
snapshot.PinnedTaskbarActions = state.PinnedTaskbarActions?.ToList() ?? [];
snapshot.EnableDynamicTaskbarActions = state.EnableDynamicTaskbarActions;
@@ -261,7 +307,19 @@ internal sealed class StatusBarSettingsService : IStatusBarSettingsService
snapshot.ClockDisplayFormat = state.ClockDisplayFormat;
snapshot.StatusBarSpacingMode = state.SpacingMode;
snapshot.StatusBarCustomSpacingPercent = state.CustomSpacingPercent;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.TopStatusComponentIds),
nameof(AppSettingsSnapshot.PinnedTaskbarActions),
nameof(AppSettingsSnapshot.EnableDynamicTaskbarActions),
nameof(AppSettingsSnapshot.TaskbarLayoutMode),
nameof(AppSettingsSnapshot.ClockDisplayFormat),
nameof(AppSettingsSnapshot.StatusBarSpacingMode),
nameof(AppSettingsSnapshot.StatusBarCustomSpacingPercent)
]);
}
}
@@ -295,12 +353,17 @@ internal sealed class WeatherProviderAdapter : IWeatherProvider, IWeatherInfoSer
internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposable
{
private readonly AppSettingsService _appSettingsService = new();
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 = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new WeatherSettingsState(
snapshot.WeatherLocationMode,
snapshot.WeatherLocationKey,
@@ -316,7 +379,7 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
public void Save(WeatherSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.WeatherLocationMode = state.LocationMode;
snapshot.WeatherLocationKey = state.LocationKey;
snapshot.WeatherLocationName = state.LocationName;
@@ -327,7 +390,22 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
snapshot.WeatherIconPackId = state.IconPackId;
snapshot.WeatherNoTlsRequests = state.NoTlsRequests;
snapshot.WeatherLocationQuery = state.LocationQuery;
_appSettingsService.Save(snapshot);
_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 IWeatherInfoService GetWeatherInfoService()
@@ -343,41 +421,74 @@ internal sealed class WeatherSettingsService : IWeatherSettingsService, IDisposa
internal sealed class RegionSettingsService : IRegionSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
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 = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new RegionSettingsState(snapshot.LanguageCode, snapshot.TimeZoneId);
}
public void Save(RegionSettingsState state)
{
var snapshot = _appSettingsService.Load();
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();
_appSettingsService.Save(snapshot);
_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 UpdateSettingsService : IUpdateSettingsService, IDisposable
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private readonly GitHubReleaseUpdateService _releaseUpdateService = new("wwiinnddyy", "LanMountainDesktop");
public UpdateSettingsService(ISettingsService settingsService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
}
public UpdateSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new UpdateSettingsState(
snapshot.AutoCheckUpdates,
snapshot.IncludePrereleaseUpdates,
@@ -386,11 +497,19 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
public void Save(UpdateSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates;
snapshot.UpdateChannel = state.UpdateChannel;
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys:
[
nameof(AppSettingsSnapshot.AutoCheckUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
nameof(AppSettingsSnapshot.UpdateChannel)
]);
}
public Task<UpdateCheckResult> CheckForUpdatesAsync(
@@ -443,11 +562,12 @@ internal sealed class LauncherPolicyService : ILauncherPolicyService
internal sealed class PluginManagementSettingsService : IPluginManagementSettingsService
{
private readonly AppSettingsService _appSettingsService = new();
private readonly ISettingsService _settingsService;
private PluginRuntimeService? _pluginRuntimeService;
public PluginManagementSettingsService(PluginRuntimeService? pluginRuntimeService)
public PluginManagementSettingsService(ISettingsService settingsService, PluginRuntimeService? pluginRuntimeService)
{
_settingsService = settingsService ?? throw new ArgumentNullException(nameof(settingsService));
_pluginRuntimeService = pluginRuntimeService;
}
@@ -458,15 +578,18 @@ internal sealed class PluginManagementSettingsService : IPluginManagementSetting
public PluginManagementSettingsState Get()
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
return new PluginManagementSettingsState(snapshot.DisabledPluginIds?.ToArray() ?? []);
}
public void Save(PluginManagementSettingsState state)
{
var snapshot = _appSettingsService.Load();
var snapshot = _settingsService.Load();
snapshot.DisabledPluginIds = state.DisabledPluginIds?.ToList() ?? [];
_appSettingsService.Save(snapshot);
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
changedKeys: [nameof(AppSettingsSnapshot.DisabledPluginIds)]);
}
public IReadOnlyList<InstalledPluginInfo> GetInstalledPlugins()
@@ -653,19 +776,19 @@ internal sealed class SettingsFacadeService : ISettingsFacadeService, IDisposabl
{
Settings = new SettingsService();
Catalog = new SettingsCatalogService();
Grid = new GridSettingsService();
Wallpaper = new WallpaperSettingsService();
Grid = new GridSettingsService(Settings);
Wallpaper = new WallpaperSettingsService(Settings);
WallpaperMedia = new WallpaperMediaService();
Theme = new ThemeAppearanceService();
StatusBar = new StatusBarSettingsService();
_weatherSettingsService = new WeatherSettingsService();
Theme = new ThemeAppearanceService(Settings);
StatusBar = new StatusBarSettingsService(Settings);
_weatherSettingsService = new WeatherSettingsService(Settings);
Weather = _weatherSettingsService;
Region = new RegionSettingsService();
_updateSettingsService = new UpdateSettingsService();
Region = new RegionSettingsService(Settings);
_updateSettingsService = new UpdateSettingsService(Settings);
Update = _updateSettingsService;
LauncherCatalog = new LauncherCatalogService();
LauncherPolicy = new LauncherPolicyService();
_pluginManagementSettingsService = new PluginManagementSettingsService(pluginRuntimeService);
_pluginManagementSettingsService = new PluginManagementSettingsService(Settings, pluginRuntimeService);
PluginManagement = _pluginManagementSettingsService;
_pluginMarketSettingsService = new PluginMarketSettingsService(pluginRuntimeService);
PluginMarket = _pluginMarketSettingsService;

View File

@@ -0,0 +1,334 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Plugins;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views.SettingsPages;
using Microsoft.Extensions.DependencyInjection;
namespace LanMountainDesktop.Services.Settings;
public sealed class SettingsPageDescriptor
{
private readonly Func<ISettingsPageHostContext, Control> _factory;
public SettingsPageDescriptor(
string pageId,
string title,
string? description,
string iconKey,
string? selectedIconKey,
SettingsPageCategory category,
int sortOrder,
string? pluginId,
bool isBuiltIn,
bool hideDefault,
bool hidePageTitle,
bool useFullWidth,
string? groupId,
Func<ISettingsPageHostContext, Control> factory)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pageId);
ArgumentException.ThrowIfNullOrWhiteSpace(title);
ArgumentException.ThrowIfNullOrWhiteSpace(iconKey);
ArgumentNullException.ThrowIfNull(factory);
PageId = pageId.Trim();
Title = title.Trim();
Description = string.IsNullOrWhiteSpace(description) ? null : description.Trim();
IconKey = iconKey.Trim();
SelectedIconKey = string.IsNullOrWhiteSpace(selectedIconKey) ? IconKey : selectedIconKey.Trim();
Category = category;
SortOrder = sortOrder;
PluginId = string.IsNullOrWhiteSpace(pluginId) ? null : pluginId.Trim();
IsBuiltIn = isBuiltIn;
HideDefault = hideDefault;
HidePageTitle = hidePageTitle;
UseFullWidth = useFullWidth;
GroupId = string.IsNullOrWhiteSpace(groupId) ? null : groupId.Trim();
_factory = factory;
}
public string PageId { get; }
public string Title { get; }
public string? Description { get; }
public string IconKey { get; }
public string SelectedIconKey { get; }
public SettingsPageCategory Category { get; }
public int SortOrder { get; }
public string? PluginId { get; }
public bool IsBuiltIn { get; }
public bool HideDefault { get; }
public bool HidePageTitle { get; }
public bool UseFullWidth { get; }
public string? GroupId { get; }
public Control CreatePage(ISettingsPageHostContext hostContext) => _factory(hostContext);
}
public interface ISettingsPageRegistry
{
void Rebuild();
IReadOnlyList<SettingsPageDescriptor> GetPages();
bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor);
}
internal sealed class SettingsPageRegistry : ISettingsPageRegistry, IDisposable
{
private readonly ISettingsFacadeService _settingsFacade;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly LocalizationService _localizationService;
private readonly Func<PluginRuntimeService?> _pluginRuntimeAccessor;
private readonly object _gate = new();
private readonly List<SettingsPageDescriptor> _pages = [];
private ServiceProvider? _hostServices;
public SettingsPageRegistry(
ISettingsFacadeService settingsFacade,
IHostApplicationLifecycle hostApplicationLifecycle,
LocalizationService localizationService,
Func<PluginRuntimeService?> pluginRuntimeAccessor)
{
_settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
_hostApplicationLifecycle = hostApplicationLifecycle ?? throw new ArgumentNullException(nameof(hostApplicationLifecycle));
_localizationService = localizationService ?? throw new ArgumentNullException(nameof(localizationService));
_pluginRuntimeAccessor = pluginRuntimeAccessor ?? throw new ArgumentNullException(nameof(pluginRuntimeAccessor));
}
public void Rebuild()
{
lock (_gate)
{
_pages.Clear();
RebuildHostServices();
RegisterAssemblyPages(
typeof(App).Assembly,
_hostServices!,
pluginId: null,
isBuiltIn: true);
var pluginRuntime = _pluginRuntimeAccessor();
if (pluginRuntime is null)
{
SortPages();
return;
}
foreach (var loadedPlugin in pluginRuntime.LoadedPlugins)
{
RegisterPluginPages(loadedPlugin);
RegisterLegacyPluginSections(loadedPlugin);
}
SortPages();
}
}
public IReadOnlyList<SettingsPageDescriptor> GetPages()
{
lock (_gate)
{
return _pages.ToArray();
}
}
public bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor)
{
ArgumentException.ThrowIfNullOrWhiteSpace(pageId);
lock (_gate)
{
descriptor = _pages.FirstOrDefault(item =>
string.Equals(item.PageId, pageId, StringComparison.OrdinalIgnoreCase));
return descriptor is not null;
}
}
public void Dispose()
{
_hostServices?.Dispose();
}
private void RebuildHostServices()
{
_hostServices?.Dispose();
var services = new ServiceCollection();
services.AddSingleton(_settingsFacade);
services.AddSingleton(_settingsFacade.Settings);
services.AddSingleton(_settingsFacade.Catalog);
services.AddSingleton(_hostApplicationLifecycle);
services.AddSingleton(_localizationService);
var pluginRuntime = _pluginRuntimeAccessor();
if (pluginRuntime is not null)
{
services.AddSingleton(pluginRuntime);
}
_hostServices = services.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = false,
ValidateOnBuild = false
});
}
private void RegisterAssemblyPages(
Assembly assembly,
IServiceProvider services,
string? pluginId,
bool isBuiltIn)
{
foreach (var pageType in assembly.GetTypes()
.Where(type => !type.IsAbstract && typeof(SettingsPageBase).IsAssignableFrom(type)))
{
var pageInfo = pageType.GetCustomAttribute<SettingsPageInfoAttribute>();
if (pageInfo is null)
{
continue;
}
var category = isBuiltIn ? pageInfo.Category : SettingsPageCategory.Plugins;
var sortOrder = isBuiltIn ? pageInfo.SortOrder : 100 + pageInfo.SortOrder;
var title = ResolveLocalizedText(pageInfo.TitleLocalizationKey, pageInfo.Name);
var description = ResolveLocalizedText(pageInfo.DescriptionLocalizationKey, null);
_pages.Add(new SettingsPageDescriptor(
pageInfo.Id,
title,
description,
pageInfo.IconKey,
pageInfo.SelectedIconKey,
category,
sortOrder,
pluginId,
isBuiltIn,
pageInfo.HideDefault,
pageInfo.HidePageTitle,
pageInfo.UseFullWidth,
pageInfo.GroupId,
hostContext => CreatePage(services, pageType, hostContext)));
}
}
private void RegisterPluginPages(LoadedPlugin loadedPlugin)
{
RegisterAssemblyPages(
loadedPlugin.Assembly,
loadedPlugin.Services,
loadedPlugin.Manifest.Id,
isBuiltIn: false);
}
private void RegisterLegacyPluginSections(LoadedPlugin loadedPlugin)
{
var localizer = PluginLocalizer.Create(loadedPlugin.RuntimeContext);
foreach (var section in loadedPlugin.SettingsSections)
{
var pageId = $"plugin:{loadedPlugin.Manifest.Id}:{section.Id}";
var title = localizer.GetString(section.TitleLocalizationKey, section.TitleLocalizationKey);
var description = string.IsNullOrWhiteSpace(section.DescriptionLocalizationKey)
? null
: localizer.GetString(section.DescriptionLocalizationKey, section.DescriptionLocalizationKey);
_pages.Add(new SettingsPageDescriptor(
pageId,
title,
description,
section.IconKey,
section.IconKey,
SettingsPageCategory.Plugins,
200 + section.SortOrder,
loadedPlugin.Manifest.Id,
isBuiltIn: false,
hideDefault: false,
hidePageTitle: false,
useFullWidth: false,
groupId: null,
hostContext =>
{
var page = new GeneratedPluginSettingsPage(
new PluginGeneratedSettingsPageViewModel(
_settingsFacade.Settings,
loadedPlugin.Manifest.Id,
section,
localizer));
page.InitializeHostContext(hostContext);
return page;
}));
}
}
private void SortPages()
{
_pages.Sort(static (left, right) =>
{
var categoryCompare = left.Category.CompareTo(right.Category);
if (categoryCompare != 0)
{
return categoryCompare;
}
var sortOrderCompare = left.SortOrder.CompareTo(right.SortOrder);
if (sortOrderCompare != 0)
{
return sortOrderCompare;
}
var pluginCompare = string.Compare(left.PluginId, right.PluginId, StringComparison.OrdinalIgnoreCase);
if (pluginCompare != 0)
{
return pluginCompare;
}
return string.Compare(left.PageId, right.PageId, StringComparison.OrdinalIgnoreCase);
});
}
private string ResolveLocalizedText(string? localizationKey, string? fallback)
{
if (string.IsNullOrWhiteSpace(localizationKey))
{
return fallback ?? string.Empty;
}
var languageCode = _settingsFacade.Region.Get().LanguageCode;
var normalizedLanguageCode = _localizationService.NormalizeLanguageCode(languageCode);
return _localizationService.GetString(
normalizedLanguageCode,
localizationKey,
string.IsNullOrWhiteSpace(fallback) ? localizationKey : fallback);
}
private static Control CreatePage(
IServiceProvider services,
Type pageType,
ISettingsPageHostContext hostContext)
{
var page = (Control)ActivatorUtilities.CreateInstance(services, pageType);
if (page is SettingsPageBase settingsPage)
{
settingsPage.InitializeHostContext(hostContext);
}
return page;
}
}

View File

@@ -0,0 +1,20 @@
using System;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop.Services;
public static class SettingsServiceAppSnapshotExtensions
{
public static AppSettingsSnapshot Load(this ISettingsService settingsService)
{
ArgumentNullException.ThrowIfNull(settingsService);
return settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
}
public static void Save(this ISettingsService settingsService, AppSettingsSnapshot snapshot)
{
ArgumentNullException.ThrowIfNull(settingsService);
settingsService.SaveSnapshot(SettingsScope.App, snapshot ?? new AppSettingsSnapshot());
}
}

View File

@@ -0,0 +1,306 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Styling;
using Avalonia.Threading;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
namespace LanMountainDesktop.Services.Settings;
public enum SettingsWindowAnchorTarget
{
DesktopDockTrailingEdge = 0
}
public enum SettingsWindowFallbackMode
{
None = 0,
ScreenBottomRight = 1
}
public readonly record struct SettingsWindowOpenRequest(
string Source,
Window? Owner = null,
string? PageId = null,
SettingsWindowAnchorTarget AnchorTarget = SettingsWindowAnchorTarget.DesktopDockTrailingEdge,
SettingsWindowFallbackMode FallbackMode = SettingsWindowFallbackMode.ScreenBottomRight);
public interface ISettingsWindowAnchorProvider
{
bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds);
}
public interface ISettingsWindowService
{
bool IsOpen { get; }
event EventHandler? StateChanged;
void Open(SettingsWindowOpenRequest request);
void Close();
void Toggle(SettingsWindowOpenRequest request);
}
internal sealed class SettingsWindowService : ISettingsWindowService
{
private readonly ISettingsPageRegistry _pageRegistry;
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService;
private SettingsWindowViewModel _viewModel = null!;
private SettingsWindow? _window;
public SettingsWindowService(
ISettingsPageRegistry pageRegistry,
IHostApplicationLifecycle hostApplicationLifecycle,
ISettingsFacadeService settingsFacade)
{
_pageRegistry = pageRegistry;
_hostApplicationLifecycle = hostApplicationLifecycle;
_settingsFacade = settingsFacade;
_localizationService = new();
_settingsFacade.Settings.Changed += OnSettingsChanged;
}
private string L(string key)
{
var regionState = _settingsFacade.Region.Get();
var languageCode = regionState.LanguageCode ?? "zh-CN";
return _localizationService.GetString(languageCode, key, key);
}
public bool IsOpen => _window is { IsVisible: true };
public event EventHandler? StateChanged;
public void Open(SettingsWindowOpenRequest request)
{
_pageRegistry.Rebuild();
_window ??= CreateWindow();
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode);
_window.ReloadPages(request.PageId);
PositionWindow(_window, request);
if (!_window.IsVisible)
{
if (request.Owner is not null && request.Owner.IsVisible)
{
_window.Show(request.Owner);
}
else
{
_window.Show();
}
NotifyStateChanged();
PositionWindowLater(_window, request);
return;
}
_window.Activate();
PositionWindowLater(_window, request);
}
public void Close()
{
_window?.Close();
}
public void Toggle(SettingsWindowOpenRequest request)
{
if (IsOpen)
{
Close();
return;
}
Open(request);
}
private SettingsWindow CreateWindow()
{
var regionState = _settingsFacade.Region.Get();
var languageCode = regionState.LanguageCode ?? "zh-CN";
_viewModel = new SettingsWindowViewModel(_localizationService, languageCode).Initialize();
var themeState = _settingsFacade.Theme.Get();
var useSystemChrome = themeState.UseSystemChrome;
var window = new SettingsWindow(
_viewModel,
_pageRegistry,
_hostApplicationLifecycle,
useSystemChrome);
ApplyTheme(window, themeState.IsNightMode);
window.ShowInTaskbar = false;
window.Closed += (_, _) =>
{
_window = null;
NotifyStateChanged();
};
return window;
}
private void PositionWindowLater(SettingsWindow window, SettingsWindowOpenRequest request)
{
Dispatcher.UIThread.Post(
() =>
{
if (!window.IsVisible)
{
return;
}
PositionWindow(window, request);
},
DispatcherPriority.Background);
}
private static void PositionWindow(SettingsWindow window, SettingsWindowOpenRequest request)
{
if (request.AnchorTarget == SettingsWindowAnchorTarget.DesktopDockTrailingEdge &&
request.Owner is ISettingsWindowAnchorProvider anchorProvider &&
anchorProvider.TryGetSettingsWindowAnchorBounds(out var anchorBounds))
{
PositionWindowAboveAnchor(window, anchorBounds, request);
return;
}
if (request.FallbackMode == SettingsWindowFallbackMode.ScreenBottomRight)
{
PositionWindowNearScreenBottomRight(window, request);
}
}
private static void PositionWindowAboveAnchor(Window window, PixelRect anchorBounds, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
if (anchorBounds.Width <= 0 || anchorBounds.Height <= 0 ||
anchorBounds.Right < workingArea.X || anchorBounds.Y > workingArea.Bottom)
{
PositionWindowNearScreenBottomRight(window, request);
return;
}
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var gap = (int)Math.Round(16 * scale);
var x = anchorBounds.Right - width - inset;
var y = anchorBounds.Y - height - gap;
x = Math.Clamp(x, workingArea.X + inset, Math.Max(workingArea.X + inset, workingArea.Right - width - inset));
y = Math.Clamp(y, workingArea.Y + inset, Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset));
window.Position = new PixelPoint(x, y);
}
private static void PositionWindowNearScreenBottomRight(Window window, SettingsWindowOpenRequest request)
{
var workingArea = GetWorkingArea(window, request);
var scale = window.RenderScaling > 0 ? window.RenderScaling : 1d;
var width = ResolveWindowWidth(window, scale);
var height = ResolveWindowHeight(window, scale);
var inset = (int)Math.Round(24 * scale);
var x = Math.Max(workingArea.X + inset, workingArea.Right - width - inset);
var y = Math.Max(workingArea.Y + inset, workingArea.Bottom - height - inset);
window.Position = new PixelPoint(x, y);
}
private static PixelRect GetWorkingArea(Window window, SettingsWindowOpenRequest request)
{
if (request.Owner is not null && request.Owner.Screens?.ScreenFromWindow(request.Owner) is { } ownerScreen)
{
return ownerScreen.WorkingArea;
}
if (window.Screens?.ScreenFromWindow(window) is { } windowScreen)
{
return windowScreen.WorkingArea;
}
return window.Screens?.Primary?.WorkingArea
?? new PixelRect(
0,
0,
Math.Max(1280, ResolveWindowWidth(window, 1d) + 96),
Math.Max(720, ResolveWindowHeight(window, 1d) + 96));
}
private static int ResolveWindowWidth(Window window, double scale)
{
var widthDip = window.Bounds.Width > 1 ? window.Bounds.Width : Math.Max(window.Width, window.MinWidth);
return Math.Max(320, (int)Math.Round(widthDip * scale));
}
private static int ResolveWindowHeight(Window window, double scale)
{
var heightDip = window.Bounds.Height > 1 ? window.Bounds.Height : Math.Max(window.Height, window.MinHeight);
return Math.Max(240, (int)Math.Round(heightDip * scale));
}
private void NotifyStateChanged()
{
StateChanged?.Invoke(this, EventArgs.Empty);
}
private void OnSettingsChanged(object? sender, SettingsChangedEvent e)
{
_ = sender;
if (e.Scope != SettingsScope.App)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
if (_window is null || _viewModel is null)
{
return;
}
var changedKeys = e.ChangedKeys?.ToArray();
var refreshAll = changedKeys is null || changedKeys.Length == 0;
var languageChanged = refreshAll || changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged)
{
var regionState = _settingsFacade.Region.Get();
_viewModel.RefreshLanguage(regionState.LanguageCode);
_pageRegistry.Rebuild();
_window.ReloadPages(_viewModel.CurrentPageId);
_window.RefreshShellText();
}
if (themeChanged)
{
var themeState = _settingsFacade.Theme.Get();
_window.ApplyChromeMode(themeState.UseSystemChrome);
ApplyTheme(_window, themeState.IsNightMode);
}
}, DispatcherPriority.Background);
}
private static void ApplyTheme(SettingsWindow window, bool isNightMode)
{
window.RequestedThemeVariant = isNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
}
}