This commit is contained in:
lincube
2026-03-22 02:53:31 +08:00
parent 73cdefe296
commit 1a7dde34d0
15 changed files with 1058 additions and 64 deletions

View File

@@ -12,12 +12,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneratedPluginSettingsPage : SettingsPageBase
{
public GeneratedPluginSettingsPage()
: this(
new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US")))
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -223,4 +218,272 @@ public partial class GeneratedPluginSettingsPage : SettingsPageBase
return textBox;
}
private static PluginGeneratedSettingsPageViewModel CreateDefaultViewModel()
{
return new PluginGeneratedSettingsPageViewModel(
HostSettingsFacadeProvider.GetOrCreate().Settings,
string.Empty,
new PluginSettingsSectionRegistration("_preview", "preview", []),
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private static PluginGeneratedSettingsPageViewModel CreateDesignTimeViewModel()
{
const string pluginId = "preview.plugin";
var settingsService = new DesignTimeSettingsService();
var section = new PluginSettingsSectionRegistration(
"desktop_preview",
"Preview Widget Settings",
[
new SettingsOptionDefinition(
"enable_glow",
SettingsOptionType.Toggle,
"Enable glow",
"Adds a soft highlight around the preview widget.",
true),
new SettingsOptionDefinition(
"refresh_minutes",
SettingsOptionType.Number,
"Refresh interval",
"How often the plugin refreshes its cached content.",
30d,
minimum: 5d,
maximum: 120d),
new SettingsOptionDefinition(
"layout_density",
SettingsOptionType.Select,
"Layout density",
"Choose how compact the widget layout should feel.",
"balanced",
[
new SettingsOptionChoice("compact", "Compact"),
new SettingsOptionChoice("balanced", "Balanced"),
new SettingsOptionChoice("comfortable", "Comfortable")
]),
new SettingsOptionDefinition(
"content_path",
SettingsOptionType.Path,
"Content folder",
"Local folder used by the plugin for mock assets.",
@"C:\Preview\PluginAssets"),
new SettingsOptionDefinition(
"keywords",
SettingsOptionType.List,
"Pinned keywords",
"Comma-separated topics that will be emphasized in the widget.",
new[] { "avalonia", "preview", "design-time" })
],
"Mock plugin settings shown only in Avalonia design mode.");
settingsService.SetValue(
SettingsScope.Plugin,
"enable_glow",
true,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"refresh_minutes",
30d,
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"layout_density",
"balanced",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"content_path",
@"C:\Preview\PluginAssets",
pluginId,
sectionId: section.Id);
settingsService.SetValue(
SettingsScope.Plugin,
"keywords",
new[] { "avalonia", "preview", "design-time" },
pluginId,
sectionId: section.Id);
return new PluginGeneratedSettingsPageViewModel(
settingsService,
pluginId,
section,
new PluginLocalizer(AppContext.BaseDirectory, "en-US"));
}
private sealed class DesignTimeSettingsService : ISettingsService
{
private readonly Dictionary<string, object?> _values = new(StringComparer.OrdinalIgnoreCase);
public event EventHandler<SettingsChangedEvent>? Changed;
public T LoadSnapshot<T>(SettingsScope scope, string? subjectId = null, string? placementId = null) where T : new()
=> new();
public void SaveSnapshot<T>(
SettingsScope scope,
T snapshot,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public T LoadSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null) where T : new()
=> new();
public void SaveSection<T>(
SettingsScope scope,
string subjectId,
string sectionId,
T section,
string? placementId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys);
}
public void DeleteSection(
SettingsScope scope,
string subjectId,
string sectionId,
string? placementId = null)
{
var prefix = BuildStorageKey(scope, subjectId, placementId, sectionId, key: null);
foreach (var existingKey in _values.Keys.Where(key => key.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)).ToArray())
{
_values.Remove(existingKey);
}
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys: null);
}
public T? GetValue<T>(
SettingsScope scope,
string key,
string? subjectId = null,
string? placementId = null,
string? sectionId = null)
{
return _values.TryGetValue(BuildStorageKey(scope, subjectId, placementId, sectionId, key), out var value)
? ConvertValue<T>(value)
: default;
}
public void SetValue<T>(
SettingsScope scope,
string key,
T value,
string? subjectId = null,
string? placementId = null,
string? sectionId = null,
IReadOnlyCollection<string>? changedKeys = null)
{
_values[BuildStorageKey(scope, subjectId, placementId, sectionId, key)] = value;
RaiseChanged(scope, subjectId, placementId, sectionId, changedKeys ?? [key]);
}
public IComponentSettingsAccessor GetComponentAccessor(string componentId, string? placementId)
{
return new DesignTimeComponentSettingsAccessor(this, componentId, placementId);
}
private static T? ConvertValue<T>(object? value)
{
if (value is null)
{
return default;
}
if (value is T typedValue)
{
return typedValue;
}
var targetType = Nullable.GetUnderlyingType(typeof(T)) ?? typeof(T);
try
{
return (T?)Convert.ChangeType(value, targetType);
}
catch
{
return default;
}
}
private static string BuildStorageKey(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
string? key)
{
return string.Join(
"|",
scope,
subjectId ?? string.Empty,
placementId ?? string.Empty,
sectionId ?? string.Empty,
key ?? string.Empty);
}
private void RaiseChanged(
SettingsScope scope,
string? subjectId,
string? placementId,
string? sectionId,
IReadOnlyCollection<string>? changedKeys)
{
Changed?.Invoke(this, new SettingsChangedEvent(scope, subjectId, placementId, sectionId, changedKeys));
}
}
private sealed class DesignTimeComponentSettingsAccessor : IComponentSettingsAccessor
{
private readonly DesignTimeSettingsService _settingsService;
public DesignTimeComponentSettingsAccessor(
DesignTimeSettingsService 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);
}
}

View File

@@ -1,3 +1,5 @@
using System;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.PluginMarket;
@@ -17,7 +19,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginMarketSettingsPage : SettingsPageBase
{
public PluginMarketSettingsPage()
: this(CreateDefaultViewModel())
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -34,6 +36,11 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -48,6 +55,113 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
new AirAppMarketReadmeService());
}
private static PluginMarketSettingsPageViewModel CreateDesignTimeViewModel()
{
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService();
var viewModel = new PluginMarketSettingsPageViewModel(
settingsFacade,
localizationService,
new AirAppMarketIconService(),
new AirAppMarketReadmeService());
var previewHostVersion = new Version(1, 2, 0);
var items = new[]
{
CreateMarketItem(
new PluginMarketPluginInfo(
"news-tiles",
"News Tiles",
"Brings editorial news cards and ticker rows to the desktop.",
"LanMountain Labs",
"1.2.0",
"1.0.0",
"1.0.0",
"https://example.com/news-tiles.zip",
"v1.2.0",
"news-tiles.zip",
string.Empty,
"https://example.com/news-tiles/readme",
"https://example.com/news-tiles",
"https://example.com/news-tiles/repo",
["news", "widgets"],
[],
DateTimeOffset.Now.AddDays(-8),
DateTimeOffset.Now.AddDays(-2)),
localizationService,
installedPlugin: null,
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"workspace-pulse",
"Workspace Pulse",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.4.0",
"1.0.0",
"1.0.0",
"https://example.com/workspace-pulse.zip",
"v2.4.0",
"workspace-pulse.zip",
string.Empty,
"https://example.com/workspace-pulse/readme",
"https://example.com/workspace-pulse",
"https://example.com/workspace-pulse/repo",
["dashboard", "productivity"],
[],
DateTimeOffset.Now.AddDays(-30),
DateTimeOffset.Now.AddDays(-1)),
localizationService,
new InstalledPluginInfo(
new PluginManifest(
"workspace-pulse",
"Workspace Pulse",
"WorkspacePulse.dll",
"Tracks active projects and shows a compact productivity summary.",
"Studio North",
"2.1.0"),
true,
true,
true,
null),
previewHostVersion),
CreateMarketItem(
new PluginMarketPluginInfo(
"glass-panels",
"Glass Panels",
"Adds experimental acrylic surfaces for plugin-powered widgets.",
"Aster Team",
"0.8.0",
"1.0.0",
"9.0.0",
"https://example.com/glass-panels.zip",
"v0.8.0",
"glass-panels.zip",
string.Empty,
"https://example.com/glass-panels/readme",
"https://example.com/glass-panels",
"https://example.com/glass-panels/repo",
["theme", "experimental"],
[],
DateTimeOffset.Now.AddDays(-12),
DateTimeOffset.Now.AddDays(-3)),
localizationService,
installedPlugin: null,
previewHostVersion)
};
foreach (var item in items)
{
viewModel.MarketPlugins.Add(item);
viewModel.FilteredPlugins.Add(item);
}
viewModel.ShowEmptyState = false;
viewModel.EmptyStateText = string.Empty;
viewModel.StatusMessage = "Showing 3 mocked marketplace plugins for Avalonia design mode.";
return viewModel;
}
private void OnRestartRequested(string? reason)
{
RequestRestart(reason ?? ViewModel.RestartRequiredMessage);
@@ -60,4 +174,17 @@ public partial class PluginMarketSettingsPage : SettingsPageBase
OpenDrawer(drawer, detailViewModel.DrawerTitle);
await detailViewModel.InitializeAsync();
}
private static PluginMarketItemViewModel CreateMarketItem(
PluginMarketPluginInfo plugin,
LocalizationService localizationService,
InstalledPluginInfo? installedPlugin,
Version hostVersion)
{
var languageCode = localizationService.NormalizeLanguageCode(
HostSettingsFacadeProvider.GetOrCreate().Region.Get().LanguageCode);
var item = new PluginMarketItemViewModel(plugin, localizationService, languageCode);
item.ApplyInstallState(installedPlugin, hostVersion);
return item;
}
}

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
@@ -15,7 +16,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class PluginsSettingsPage : SettingsPageBase
{
public PluginsSettingsPage()
: this(new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
}
@@ -31,6 +32,11 @@ public partial class PluginsSettingsPage : SettingsPageBase
public override async void OnNavigatedTo(object? parameter)
{
if (Design.IsDesignMode)
{
return;
}
await ViewModel.InitializeAsync();
}
@@ -38,4 +44,47 @@ public partial class PluginsSettingsPage : SettingsPageBase
{
RequestRestart(ViewModel.RestartRequiredMessage);
}
private static PluginsSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate());
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"calendar-plus",
"Calendar Plus",
"CalendarPlus.dll",
"Adds a compact agenda widget and richer date cards.",
"LanMountain Labs",
"1.4.0"),
true,
true,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"focus-mode",
"Focus Mode",
"FocusMode.dll",
"Provides a distraction-free overlay and quick toggles.",
"Studio North",
"0.9.2"),
true,
false,
true,
null)));
viewModel.InstalledPlugins.Add(new InstalledPluginItemViewModel(new InstalledPluginInfo(
new PluginManifest(
"notes-dock",
"Notes Dock",
"NotesDock.dll",
"Pins short markdown notes directly on the desktop.",
"Aster Team",
"2.1.0"),
false,
false,
true,
null)));
viewModel.StatusMessage = "Loaded 3 mocked plugins for Avalonia design mode.";
return viewModel;
}
}

View File

@@ -1,3 +1,4 @@
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Services.Settings;
@@ -16,7 +17,7 @@ namespace LanMountainDesktop.Views.SettingsPages;
public partial class WeatherSettingsPage : SettingsPageBase
{
public WeatherSettingsPage()
: this(CreateDefaultViewModel())
: this(Design.IsDesignMode ? CreateDesignTimeViewModel() : CreateDefaultViewModel())
{
}
@@ -29,7 +30,7 @@ public partial class WeatherSettingsPage : SettingsPageBase
public WeatherSettingsPageViewModel ViewModel { get; }
private static WeatherSettingsPageViewModel CreateDefaultViewModel()
private static WeatherSettingsPageViewModel CreateDefaultViewModel(bool enableStartupPreviewRefresh = true)
{
var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
var localizationService = new LocalizationService();
@@ -42,6 +43,14 @@ public partial class WeatherSettingsPage : SettingsPageBase
settingsFacade,
localizationService,
locationService,
weatherLocationRefreshService);
weatherLocationRefreshService,
enableStartupPreviewRefresh);
}
private static WeatherSettingsPageViewModel CreateDesignTimeViewModel()
{
var viewModel = CreateDefaultViewModel(enableStartupPreviewRefresh: false);
viewModel.ApplyDesignTimePreview();
return viewModel;
}
}