mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 15:44:25 +08:00
settings_re4
This commit is contained in:
32
.trae/specs/settings-page-fluent-redesign/checklist.md
Normal file
32
.trae/specs/settings-page-fluent-redesign/checklist.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Checklist - 设置页面 Fluent 设计改造
|
||||
|
||||
## Phase 1: 分析与准备
|
||||
|
||||
- [ ] SettingsExpander 控件分析完成
|
||||
- [ ] 当前布局问题定位完成
|
||||
|
||||
## Phase 2: 窗口布局调整
|
||||
|
||||
- [ ] SettingsWindow 内容区域无额外 Border 包裹
|
||||
- [ ] 窗口整体视觉效果正常
|
||||
- [ ] 窗口圆角在不同模式下正确显示
|
||||
|
||||
## Phase 3: 设置页面改造
|
||||
|
||||
- [ ] AppearanceSettingsPage 无额外边框包裹
|
||||
- [ ] GeneralSettingsPage 无额外边框包裹
|
||||
- [ ] ComponentsSettingsPage 无额外边框包裹
|
||||
- [ ] PluginsSettingsPage 无额外边框包裹
|
||||
- [ ] AboutSettingsPage 无额外边框包裹
|
||||
|
||||
## Phase 4: 视觉规范
|
||||
|
||||
- [ ] 设置项间距统一
|
||||
- [ ] 圆角样式统一
|
||||
- [ ] 页面标题样式统一
|
||||
|
||||
## 验证
|
||||
|
||||
- [ ] 编译通过,无错误
|
||||
- [ ] 运行正常,设置页面可正常显示
|
||||
- [ ] 视觉效果符合 Fluent 设计风格
|
||||
76
.trae/specs/settings-page-fluent-redesign/spec.md
Normal file
76
.trae/specs/settings-page-fluent-redesign/spec.md
Normal file
@@ -0,0 +1,76 @@
|
||||
# 设置页面 Fluent 设计改造规格说明书
|
||||
|
||||
## Why
|
||||
|
||||
当前 LanMountainDesktop 设置页面存在以下问题:
|
||||
1. 右侧详细设置区域被额外边框包裹,未能实现 Fluent Avalonia 控件的完整填充效果
|
||||
2. 设置项未采用 Fluent 卡片设计风格,仍使用传统 Border + StackPanel 布局
|
||||
3. 与 ClassIsland 项目的视觉风格差异较大
|
||||
|
||||
## What Changes
|
||||
|
||||
- 移除页面内容区域的额外 Border 包裹,直接使用 ScrollViewer + StackPanel
|
||||
- 参考 ClassIsland 项目,引入 SettingsExpander 控件替代传统布局
|
||||
- 统一设置项的间距、圆角、字体等视觉规范
|
||||
- 修改窗口布局,移除内容区域的 glass-panel 样式
|
||||
|
||||
## Impact
|
||||
|
||||
### Affected specs
|
||||
- 设置页面 UI 布局规范
|
||||
- Fluent 设计风格适配
|
||||
|
||||
### Affected code
|
||||
- `Views/SettingsPages/*.axaml` - 所有设置页面
|
||||
- `Views/SettingsWindow.axaml` - 设置窗口布局
|
||||
- `Styles/GlassModule.axaml` - 样式资源
|
||||
|
||||
---
|
||||
|
||||
## ADDED Requirements
|
||||
|
||||
### Requirement: 设置页面 Fluent 卡片设计
|
||||
|
||||
系统 SHALL 提供类似 ClassIsland 的 SettingsExpander 卡片式设置项。
|
||||
|
||||
#### Scenario: 设置页面布局
|
||||
- **WHEN** 用户打开任意设置页面
|
||||
- **THEN** 页面使用 ScrollViewer 直接包裹内容,无额外 Border 包裹
|
||||
- **AND THEN** 设置项使用 SettingsExpander 或 Fluent 卡片样式
|
||||
|
||||
### Requirement: 移除内容区域额外边框
|
||||
|
||||
系统 SHALL 移除右侧内容区域的 glass-panel 边框包裹。
|
||||
|
||||
#### Scenario: 内容区域无额外边框
|
||||
- **WHEN** 用户查看设置页面内容
|
||||
- **THEN** 内容直接显示在透明背景上,无额外边框包裹
|
||||
|
||||
### Requirement: 设置项视觉规范
|
||||
|
||||
系统 SHALL 统一设置项的视觉样式。
|
||||
|
||||
#### Scenario: 设置项样式
|
||||
- **WHEN** 开发者创建新的设置项
|
||||
- **THEN** 使用统一的间距(Spacing)、圆角、字体大小
|
||||
- **AND THEN** 参考 ClassIsland 的 SettingsExpander 样式
|
||||
|
||||
---
|
||||
|
||||
## MODIFIED Requirements
|
||||
|
||||
### Requirement: 设置页面布局结构
|
||||
|
||||
**当前**: Border → ScrollViewer → Border → StackPanel → 内容
|
||||
|
||||
**修改后**: ScrollViewer → StackPanel → 设置项(无额外 Border)
|
||||
|
||||
---
|
||||
|
||||
## REMOVED Requirements
|
||||
|
||||
### Requirement: 传统 Border 包裹布局
|
||||
|
||||
**Reason**: 实现 Fluent 设计风格,移除视觉噪音
|
||||
|
||||
**Migration**: 将现有 Border 包裹改为直接内容布局
|
||||
51
.trae/specs/settings-page-fluent-redesign/tasks.md
Normal file
51
.trae/specs/settings-page-fluent-redesign/tasks.md
Normal file
@@ -0,0 +1,51 @@
|
||||
# Tasks - 设置页面 Fluent 设计改造
|
||||
|
||||
## Phase 1: 分析与准备
|
||||
|
||||
- [ ] Task 1.1: 分析 ClassIsland SettingsExpander 控件实现
|
||||
- [ ] 查看 ClassIsland.Core 中的 SettingsExpander 定义
|
||||
- [ ] 分析样式模板和视觉效果
|
||||
- [ ] 确定是否需要自定义控件或使用现有替代方案
|
||||
|
||||
- [ ] Task 1.2: 分析当前设置页面布局问题
|
||||
- [ ] 定位右侧内容区域的 Border 包裹代码
|
||||
- [ ] 分析 glass-panel 样式对布局的影响
|
||||
|
||||
## Phase 2: 窗口布局调整
|
||||
|
||||
- [ ] Task 2.1: 修改 SettingsWindow.axaml 内容区域布局
|
||||
- [ ] 移除 Frame 外部的 glass-panel Border
|
||||
- [ ] 直接使用透明背景
|
||||
- [ ] 验证窗口整体视觉效果
|
||||
|
||||
## Phase 3: 设置页面改造
|
||||
|
||||
- [ ] Task 3.1: 改造 AppearanceSettingsPage 页面
|
||||
- [ ] 移除外部的 glass-panel Border
|
||||
- [ ] 调整内容布局为直接填充
|
||||
- [ ] 验证视觉效果
|
||||
|
||||
- [ ] Task 3.2: 改造 GeneralSettingsPage 页面
|
||||
- [ ] 移除外部的 glass-panel Border
|
||||
- [ ] 调整内容布局
|
||||
|
||||
- [ ] Task 3.3: 改造其他设置页面
|
||||
- [ ] ComponentsSettingsPage
|
||||
- [ ] PluginsSettingsPage
|
||||
- [ ] AboutSettingsPage
|
||||
|
||||
## Phase 4: 视觉规范统一
|
||||
|
||||
- [ ] Task 4.1: 统一设置项间距和圆角
|
||||
- [ ] 定义统一的 Spacing 值
|
||||
- [ ] 统一圆角大小
|
||||
|
||||
- [ ] Task 4.2: 优化页面标题区域样式
|
||||
- [ ] 调整 Page Header 字体大小
|
||||
- [ ] 优化 Description 样式
|
||||
|
||||
## Task Dependencies
|
||||
- Task 1.2 依赖 Task 1.1
|
||||
- Task 2.1 依赖 Task 1.2
|
||||
- Task 3.x 依赖 Task 2.1
|
||||
- Task 4.x 依赖 Task 3.x
|
||||
12
LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs
Normal file
12
LanMountainDesktop.PluginSdk/ISettingsPageHostContext.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public interface ISettingsPageHostContext
|
||||
{
|
||||
void OpenDrawer(Control content, string? title = null);
|
||||
|
||||
void CloseDrawer();
|
||||
|
||||
void RequestRestart(string? reason = null);
|
||||
}
|
||||
@@ -85,7 +85,10 @@ public sealed record PluginManifest(
|
||||
if (requestedVersion.Major != currentVersion.Major)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}', but the host provides '{PluginSdkInfo.ApiVersion}'. Upgrade the plugin to API {PluginSdkInfo.ApiVersion}.");
|
||||
$"Plugin '{normalized.Id}' targets API version '{normalized.ApiVersion}' (major {requestedVersion.Major}), " +
|
||||
$"but the host provides '{PluginSdkInfo.ApiVersion}' (major {currentVersion.Major}). " +
|
||||
$"This host only supports v{currentVersion.Major}.x plugins. " +
|
||||
$"Migrate the plugin to API {PluginSdkInfo.ApiVersion} and rebuild the package.");
|
||||
}
|
||||
|
||||
return normalized;
|
||||
|
||||
@@ -2,7 +2,7 @@ namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public static class PluginSdkInfo
|
||||
{
|
||||
public const string ApiVersion = "2.0.0";
|
||||
public const string ApiVersion = "3.0.0";
|
||||
public const string ManifestFileName = "plugin.json";
|
||||
public const string PackageFileExtension = ".laapp";
|
||||
public const string DataDirectoryName = "Data";
|
||||
|
||||
54
LanMountainDesktop.PluginSdk/SettingsPageBase.cs
Normal file
54
LanMountainDesktop.PluginSdk/SettingsPageBase.cs
Normal file
@@ -0,0 +1,54 @@
|
||||
using System;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public abstract class SettingsPageBase : UserControl
|
||||
{
|
||||
public static readonly string DialogHostIdentifier = "LanMountainDesktop.SettingsWindow";
|
||||
|
||||
private ISettingsPageHostContext? _hostContext;
|
||||
|
||||
public ISettingsPageHostContext? HostContext => _hostContext;
|
||||
|
||||
public Uri? NavigationUri { get; set; }
|
||||
|
||||
public void InitializeHostContext(ISettingsPageHostContext hostContext)
|
||||
{
|
||||
_hostContext = hostContext;
|
||||
}
|
||||
|
||||
public virtual void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
}
|
||||
|
||||
protected void OpenDrawer(Control content, string? title = null)
|
||||
{
|
||||
_hostContext?.OpenDrawer(content, title);
|
||||
}
|
||||
|
||||
protected void OpenDrawer(object content, bool usePageDataContext = false, object? dataContext = null, string? title = null)
|
||||
{
|
||||
if (content is Control control && !usePageDataContext)
|
||||
{
|
||||
control.DataContext = dataContext ?? DataContext ?? this;
|
||||
OpenDrawer(control, title);
|
||||
return;
|
||||
}
|
||||
|
||||
if (content is Control drawerControl)
|
||||
{
|
||||
OpenDrawer(drawerControl, title);
|
||||
}
|
||||
}
|
||||
|
||||
protected void CloseDrawer()
|
||||
{
|
||||
_hostContext?.CloseDrawer();
|
||||
}
|
||||
|
||||
protected void RequestRestart(string? reason = null)
|
||||
{
|
||||
_hostContext?.RequestRestart(reason);
|
||||
}
|
||||
}
|
||||
10
LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
Normal file
10
LanMountainDesktop.PluginSdk/SettingsPageCategory.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
public enum SettingsPageCategory
|
||||
{
|
||||
General = 0,
|
||||
Appearance = 10,
|
||||
Components = 20,
|
||||
Plugins = 30,
|
||||
About = 40
|
||||
}
|
||||
46
LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs
Normal file
46
LanMountainDesktop.PluginSdk/SettingsPageInfoAttribute.cs
Normal file
@@ -0,0 +1,46 @@
|
||||
using System;
|
||||
|
||||
namespace LanMountainDesktop.PluginSdk;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = false)]
|
||||
public sealed class SettingsPageInfoAttribute : Attribute
|
||||
{
|
||||
public SettingsPageInfoAttribute(
|
||||
string id,
|
||||
string name,
|
||||
SettingsPageCategory category)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(id);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(name);
|
||||
|
||||
Id = id.Trim();
|
||||
Name = name.Trim();
|
||||
Category = category;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public SettingsPageCategory Category { get; }
|
||||
|
||||
public string? TitleLocalizationKey { get; init; }
|
||||
|
||||
public string? DescriptionLocalizationKey { get; init; }
|
||||
|
||||
public string IconKey { get; init; } = "Settings";
|
||||
|
||||
public string? SelectedIconKey { get; init; }
|
||||
|
||||
public int SortOrder { get; init; }
|
||||
|
||||
public bool HideDefault { get; init; }
|
||||
|
||||
public bool HidePageTitle { get; init; }
|
||||
|
||||
public bool UseFullWidth { get; init; }
|
||||
|
||||
public string? GroupId { get; init; }
|
||||
|
||||
public SettingsScope Scope { get; init; } = SettingsScope.App;
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:sty="using:FluentAvalonia.Styling"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
@@ -22,6 +22,7 @@
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/FluttermotionToken.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/GlassModule.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsAnimations.axaml" />
|
||||
<StyleInclude Source="avares://LanMountainDesktop/Styles/SettingsCardStyles.axaml" />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="{DynamicResource AppFontFamily}" />
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
@@ -7,6 +9,7 @@ using Avalonia.Data.Core;
|
||||
using Avalonia.Data.Core.Plugins;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.Platform;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using AvaloniaWebView;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
@@ -38,6 +41,9 @@ public partial class App : Application
|
||||
private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
|
||||
private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService();
|
||||
private ISettingsPageRegistry? _settingsPageRegistry;
|
||||
private ISettingsWindowService? _settingsWindowService;
|
||||
private bool _exitCleanupCompleted;
|
||||
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
|
||||
private ShutdownIntent _shutdownIntent;
|
||||
@@ -46,6 +52,7 @@ public partial class App : Application
|
||||
private PluginRuntimeService? _pluginRuntimeService;
|
||||
private MainWindow? _mainWindow;
|
||||
private bool _mainWindowClosed;
|
||||
private bool _uiUnhandledExceptionHooked;
|
||||
|
||||
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
|
||||
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
|
||||
@@ -54,12 +61,18 @@ public partial class App : Application
|
||||
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
|
||||
public ISettingsFacadeService SettingsFacade => _settingsFacade;
|
||||
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
|
||||
internal ISettingsWindowService? SettingsWindowService => _settingsWindowService;
|
||||
|
||||
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
|
||||
{
|
||||
EnsureSettingsWindowService();
|
||||
AppLogger.Info(
|
||||
"SettingsFacade",
|
||||
$"Settings UI entry is disabled by hard-cut migration. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
||||
$"Opening settings window. Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
|
||||
_settingsWindowService?.Open(new SettingsWindowOpenRequest(
|
||||
Source: source,
|
||||
Owner: _mainWindow is { IsVisible: true } ? _mainWindow : null,
|
||||
PageId: pageTag));
|
||||
}
|
||||
|
||||
public override void Initialize()
|
||||
@@ -68,11 +81,15 @@ public partial class App : Application
|
||||
ConfigureWebViewUserDataFolder();
|
||||
AvaloniaWebViewBuilder.Initialize(default);
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
ApplyInitialThemeVariantFromSettings();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
EnsureSettingsWindowService();
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
AppLogger.Info("App", "Framework initialization completed.");
|
||||
RegisterUiUnhandledExceptionGuard();
|
||||
LinuxDesktopEntryInstaller.EnsureInstalled();
|
||||
InitializePluginRuntime();
|
||||
AppSettingsService.SettingsSaved += OnAppSettingsSaved;
|
||||
@@ -116,6 +133,25 @@ public partial class App : Application
|
||||
Reason: "User selected Restart App from the tray menu."));
|
||||
}
|
||||
|
||||
private void OnTraySettingsClick(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
OpenIndependentSettingsModule("TrayMenu");
|
||||
}
|
||||
|
||||
private void OnTrayComponentLibraryClick(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (_mainWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_detachedComponentLibraryWindowService.Open(_mainWindow);
|
||||
}
|
||||
|
||||
private void DisableAvaloniaDataAnnotationValidation()
|
||||
{
|
||||
// Get an array of plugins to remove
|
||||
@@ -204,6 +240,14 @@ public partial class App : Application
|
||||
showDesktopItem.Click += OnTrayShowDesktopClick;
|
||||
menu.Items.Add(showDesktopItem);
|
||||
|
||||
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
|
||||
settingsItem.Click += OnTraySettingsClick;
|
||||
menu.Items.Add(settingsItem);
|
||||
|
||||
var componentLibraryItem = new NativeMenuItem(L("tray.menu.component_library", "Component Library"));
|
||||
componentLibraryItem.Click += OnTrayComponentLibraryClick;
|
||||
menu.Items.Add(componentLibraryItem);
|
||||
|
||||
menu.Items.Add(new NativeMenuItemSeparator());
|
||||
|
||||
var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
|
||||
@@ -235,6 +279,48 @@ public partial class App : Application
|
||||
_trayIcons = null;
|
||||
}
|
||||
|
||||
private void EnsureSettingsWindowService()
|
||||
{
|
||||
_settingsPageRegistry ??= new SettingsPageRegistry(
|
||||
_settingsFacade,
|
||||
_hostApplicationLifecycle,
|
||||
_localizationService,
|
||||
() => _pluginRuntimeService);
|
||||
_settingsWindowService ??= new SettingsWindowService(
|
||||
_settingsPageRegistry,
|
||||
_hostApplicationLifecycle,
|
||||
_settingsFacade);
|
||||
}
|
||||
|
||||
private void ApplyInitialThemeVariantFromSettings()
|
||||
{
|
||||
var themeState = _settingsFacade.Theme.Get();
|
||||
RequestedThemeVariant = themeState.IsNightMode
|
||||
? ThemeVariant.Dark
|
||||
: ThemeVariant.Light;
|
||||
}
|
||||
|
||||
private void ApplyCurrentCultureFromSettings()
|
||||
{
|
||||
var snapshot = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
var languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
|
||||
CultureInfo culture;
|
||||
try
|
||||
{
|
||||
culture = CultureInfo.GetCultureInfo(languageCode);
|
||||
}
|
||||
catch (CultureNotFoundException)
|
||||
{
|
||||
culture = CultureInfo.GetCultureInfo("zh-CN");
|
||||
}
|
||||
|
||||
CultureInfo.DefaultThreadCurrentCulture = culture;
|
||||
CultureInfo.DefaultThreadCurrentUICulture = culture;
|
||||
Thread.CurrentThread.CurrentCulture = culture;
|
||||
Thread.CurrentThread.CurrentUICulture = culture;
|
||||
}
|
||||
|
||||
private void ActivateMainWindow()
|
||||
{
|
||||
RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance");
|
||||
@@ -338,6 +424,8 @@ public partial class App : Application
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
ApplyInitialThemeVariantFromSettings();
|
||||
ApplyCurrentCultureFromSettings();
|
||||
if (_trayIcons is not null)
|
||||
{
|
||||
InitializeTrayIcon();
|
||||
@@ -345,6 +433,43 @@ public partial class App : Application
|
||||
}, DispatcherPriority.Background);
|
||||
}
|
||||
|
||||
private void RegisterUiUnhandledExceptionGuard()
|
||||
{
|
||||
if (_uiUnhandledExceptionHooked)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Dispatcher.UIThread.UnhandledException += OnUiThreadUnhandledException;
|
||||
_uiUnhandledExceptionHooked = true;
|
||||
}
|
||||
|
||||
private void OnUiThreadUnhandledException(object? sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (!IsKnownWebViewStartupException(e.Exception))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
e.Handled = true;
|
||||
AppLogger.Warn(
|
||||
"WebView2",
|
||||
"Suppressed a known WebView startup exception from AvaloniaWebView.Navigate to keep the host process alive.",
|
||||
e.Exception);
|
||||
}
|
||||
|
||||
private static bool IsKnownWebViewStartupException(Exception exception)
|
||||
{
|
||||
if (exception is not NullReferenceException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stackTrace = exception.StackTrace ?? string.Empty;
|
||||
return stackTrace.Contains("AvaloniaWebView.WebView.Navigate", StringComparison.Ordinal) &&
|
||||
stackTrace.Contains("AvaloniaWebView.WebView.OnAttachedToVisualTree", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private void PerformExitCleanup()
|
||||
{
|
||||
if (_exitCleanupCompleted)
|
||||
@@ -368,6 +493,12 @@ public partial class App : Application
|
||||
_pluginRuntimeService = null;
|
||||
}
|
||||
|
||||
_settingsWindowService?.Close();
|
||||
if (_settingsPageRegistry is IDisposable disposableRegistry)
|
||||
{
|
||||
disposableRegistry.Dispose();
|
||||
}
|
||||
|
||||
AudioRecorderServiceFactory.DisposeSharedServices();
|
||||
StudyAnalyticsServiceFactory.DisposeSharedService();
|
||||
DisposeTrayIcon();
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public sealed record DesktopComponentRuntimeContext(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor);
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
namespace LanMountainDesktop.ComponentSystem;
|
||||
|
||||
public sealed record DesktopComponentSettingsContext(
|
||||
string ComponentId,
|
||||
string? PlacementId,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore);
|
||||
|
||||
public interface IComponentSettingsContextAware
|
||||
{
|
||||
void SetComponentSettingsContext(DesktopComponentSettingsContext context);
|
||||
}
|
||||
37
LanMountainDesktop/Controls/SettingsOptionCard.axaml
Normal file
37
LanMountainDesktop/Controls/SettingsOptionCard.axaml
Normal file
@@ -0,0 +1,37 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LanMountainDesktop.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Controls.SettingsOptionCard"
|
||||
x:Name="Root">
|
||||
<Border Classes="settings-option-card">
|
||||
<Grid RowDefinitions="Auto,Auto"
|
||||
RowSpacing="12">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto"
|
||||
ColumnSpacing="14">
|
||||
<Border x:Name="IconHost"
|
||||
Classes="settings-option-card-icon-host">
|
||||
<fi:SymbolIcon x:Name="CardIcon"
|
||||
Classes="icon-m" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4"
|
||||
VerticalAlignment="Center">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Classes="settings-item-label" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="settings-item-description" />
|
||||
</StackPanel>
|
||||
|
||||
<ContentPresenter x:Name="ActionContentHost"
|
||||
Grid.Column="2"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter x:Name="DetailsContentHost"
|
||||
Grid.Row="1"
|
||||
Margin="54,0,0,0" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
112
LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs
Normal file
112
LanMountainDesktop/Controls/SettingsOptionCard.axaml.cs
Normal file
@@ -0,0 +1,112 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using FluentIcons.Common;
|
||||
|
||||
namespace LanMountainDesktop.Controls;
|
||||
|
||||
public partial class SettingsOptionCard : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string?> IconKeyProperty =
|
||||
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(IconKey), "Settings");
|
||||
|
||||
public static readonly StyledProperty<string?> TitleProperty =
|
||||
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(Title));
|
||||
|
||||
public static readonly StyledProperty<string?> DescriptionProperty =
|
||||
AvaloniaProperty.Register<SettingsOptionCard, string?>(nameof(Description));
|
||||
|
||||
public static readonly StyledProperty<object?> ActionContentProperty =
|
||||
AvaloniaProperty.Register<SettingsOptionCard, object?>(nameof(ActionContent));
|
||||
|
||||
public static readonly StyledProperty<object?> DetailsContentProperty =
|
||||
AvaloniaProperty.Register<SettingsOptionCard, object?>(nameof(DetailsContent));
|
||||
|
||||
public SettingsOptionCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
RefreshVisualState();
|
||||
}
|
||||
|
||||
public string? IconKey
|
||||
{
|
||||
get => GetValue(IconKeyProperty);
|
||||
set => SetValue(IconKeyProperty, value);
|
||||
}
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public string? Description
|
||||
{
|
||||
get => GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
|
||||
public object? ActionContent
|
||||
{
|
||||
get => GetValue(ActionContentProperty);
|
||||
set => SetValue(ActionContentProperty, value);
|
||||
}
|
||||
|
||||
public object? DetailsContent
|
||||
{
|
||||
get => GetValue(DetailsContentProperty);
|
||||
set => SetValue(DetailsContentProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == IconKeyProperty ||
|
||||
change.Property == TitleProperty ||
|
||||
change.Property == DescriptionProperty ||
|
||||
change.Property == ActionContentProperty ||
|
||||
change.Property == DetailsContentProperty)
|
||||
{
|
||||
RefreshVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshVisualState()
|
||||
{
|
||||
if (CardIcon is null ||
|
||||
IconHost is null ||
|
||||
TitleTextBlock is null ||
|
||||
DescriptionTextBlock is null ||
|
||||
ActionContentHost is null ||
|
||||
DetailsContentHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CardIcon.Symbol = MapIcon(IconKey);
|
||||
IconHost.IsVisible = !string.IsNullOrWhiteSpace(IconKey);
|
||||
|
||||
TitleTextBlock.Text = Title ?? string.Empty;
|
||||
DescriptionTextBlock.Text = Description ?? string.Empty;
|
||||
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(Description);
|
||||
|
||||
ActionContentHost.Content = ActionContent;
|
||||
ActionContentHost.IsVisible = ActionContent is not null;
|
||||
|
||||
DetailsContentHost.Content = DetailsContent;
|
||||
DetailsContentHost.IsVisible = DetailsContent is not null;
|
||||
}
|
||||
|
||||
private static Symbol MapIcon(string? iconKey)
|
||||
{
|
||||
return iconKey?.Trim() switch
|
||||
{
|
||||
"DesignIdeas" => Symbol.Color,
|
||||
"GridDots" => Symbol.GridDots,
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
_ => Symbol.Settings
|
||||
};
|
||||
}
|
||||
}
|
||||
33
LanMountainDesktop/Controls/SettingsSectionCard.axaml
Normal file
33
LanMountainDesktop/Controls/SettingsSectionCard.axaml
Normal file
@@ -0,0 +1,33 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="using:LanMountainDesktop.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Controls.SettingsSectionCard"
|
||||
x:Name="Root">
|
||||
<Border Classes="settings-section-card">
|
||||
<Grid RowDefinitions="Auto,Auto"
|
||||
RowSpacing="16">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="14">
|
||||
<Border x:Name="IconHost"
|
||||
Classes="settings-section-card-icon-host">
|
||||
<fi:SymbolIcon x:Name="CardIcon"
|
||||
Classes="icon-l" />
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Spacing="4">
|
||||
<TextBlock x:Name="TitleTextBlock"
|
||||
Classes="settings-card-header"
|
||||
Margin="0" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="settings-card-description"
|
||||
Margin="0" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<ContentPresenter x:Name="CardContentHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
97
LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs
Normal file
97
LanMountainDesktop/Controls/SettingsSectionCard.axaml.cs
Normal file
@@ -0,0 +1,97 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using FluentIcons.Common;
|
||||
|
||||
namespace LanMountainDesktop.Controls;
|
||||
|
||||
public partial class SettingsSectionCard : UserControl
|
||||
{
|
||||
public static readonly StyledProperty<string?> IconKeyProperty =
|
||||
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(IconKey), "Settings");
|
||||
|
||||
public static readonly StyledProperty<string?> TitleProperty =
|
||||
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(Title));
|
||||
|
||||
public static readonly StyledProperty<string?> DescriptionProperty =
|
||||
AvaloniaProperty.Register<SettingsSectionCard, string?>(nameof(Description));
|
||||
|
||||
public static readonly StyledProperty<object?> CardContentProperty =
|
||||
AvaloniaProperty.Register<SettingsSectionCard, object?>(nameof(CardContent));
|
||||
|
||||
public SettingsSectionCard()
|
||||
{
|
||||
InitializeComponent();
|
||||
RefreshVisualState();
|
||||
}
|
||||
|
||||
public string? IconKey
|
||||
{
|
||||
get => GetValue(IconKeyProperty);
|
||||
set => SetValue(IconKeyProperty, value);
|
||||
}
|
||||
|
||||
public string? Title
|
||||
{
|
||||
get => GetValue(TitleProperty);
|
||||
set => SetValue(TitleProperty, value);
|
||||
}
|
||||
|
||||
public string? Description
|
||||
{
|
||||
get => GetValue(DescriptionProperty);
|
||||
set => SetValue(DescriptionProperty, value);
|
||||
}
|
||||
|
||||
public object? CardContent
|
||||
{
|
||||
get => GetValue(CardContentProperty);
|
||||
set => SetValue(CardContentProperty, value);
|
||||
}
|
||||
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
if (change.Property == IconKeyProperty ||
|
||||
change.Property == TitleProperty ||
|
||||
change.Property == DescriptionProperty ||
|
||||
change.Property == CardContentProperty)
|
||||
{
|
||||
RefreshVisualState();
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshVisualState()
|
||||
{
|
||||
if (CardIcon is null ||
|
||||
IconHost is null ||
|
||||
TitleTextBlock is null ||
|
||||
DescriptionTextBlock is null ||
|
||||
CardContentHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
CardIcon.Symbol = MapIcon(IconKey);
|
||||
IconHost.IsVisible = !string.IsNullOrWhiteSpace(IconKey);
|
||||
|
||||
TitleTextBlock.Text = Title ?? string.Empty;
|
||||
DescriptionTextBlock.Text = Description ?? string.Empty;
|
||||
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(Description);
|
||||
|
||||
CardContentHost.Content = CardContent;
|
||||
}
|
||||
|
||||
private static Symbol MapIcon(string? iconKey)
|
||||
{
|
||||
return iconKey?.Trim() switch
|
||||
{
|
||||
"DesignIdeas" => Symbol.Color,
|
||||
"GridDots" => Symbol.GridDots,
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
_ => Symbol.Settings
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
{
|
||||
"app.title": "LanMountainDesktop",
|
||||
"tray.tooltip": "LanMountainDesktop",
|
||||
"tray.menu.show_desktop": "Open Desktop",
|
||||
"tray.menu.settings": "Settings",
|
||||
"tray.menu.component_library": "Component Library",
|
||||
"tray.menu.restart": "Restart App",
|
||||
"tray.menu.exit": "Exit App",
|
||||
"button.back_to_windows": "Back to Windows",
|
||||
@@ -221,6 +222,32 @@
|
||||
"settings.region.timezone_header": "Time Zone",
|
||||
"settings.region.timezone_desc": "Select a time zone. Clock and calendar widgets will follow this zone.",
|
||||
"settings.region.applied_format": "Language switched to: {0}",
|
||||
"settings.region.follow_system": "Follow system default",
|
||||
"settings.general.title": "General",
|
||||
"settings.general.description": "Adjust language, time zone, and runtime behavior.",
|
||||
"settings.general.basic_header": "Basic Settings",
|
||||
"settings.general.runtime_header": "Runtime",
|
||||
"settings.general.preview_header": "Date & Time Preview",
|
||||
"settings.general.preview_time_label": "Time",
|
||||
"settings.general.preview_date_label": "Date",
|
||||
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
|
||||
"settings.appearance.title": "Appearance",
|
||||
"settings.appearance.description": "Adjust theme, wallpaper, and status bar presentation.",
|
||||
"settings.appearance.theme_header": "Theme",
|
||||
"settings.color.enable_night_mode_toggle": "Enable night mode",
|
||||
"settings.color.use_system_chrome_toggle": "Use system window chrome",
|
||||
"settings.color.theme_color_label": "Theme accent color",
|
||||
"settings.wallpaper.placement.fill": "Fill",
|
||||
"settings.wallpaper.placement.fit": "Fit",
|
||||
"settings.wallpaper.placement.stretch": "Stretch",
|
||||
"settings.wallpaper.placement.center": "Center",
|
||||
"settings.wallpaper.placement.tile": "Tile",
|
||||
"settings.status_bar.clock_format_label": "Clock format",
|
||||
"settings.status_bar.clock_format.hm": "Hour:Minute",
|
||||
"settings.status_bar.clock_format.hms": "Hour:Minute:Second",
|
||||
"settings.components.title": "Components",
|
||||
"settings.components.description": "Adjust desktop grid density and widget placement.",
|
||||
"settings.components.grid_header": "Grid Layout",
|
||||
"settings.update.title": "Update",
|
||||
"settings.update.current_version_label": "Current Version",
|
||||
"settings.update.latest_version_label": "Latest Release",
|
||||
@@ -273,6 +300,12 @@
|
||||
"settings.about.render_mode.current_format": "Current backend: {0}",
|
||||
"settings.about.render_mode.impl_format": "Runtime implementation: {0}",
|
||||
"settings.about.render_mode.impl_unavailable": "Runtime implementation details are unavailable.",
|
||||
"settings.about.description": "Application details and update preferences.",
|
||||
"settings.about.app_info_header": "Application Information",
|
||||
"settings.about.update_header": "Updates",
|
||||
"settings.about.version_label": "Version",
|
||||
"settings.about.render_backend_label": "Render Backend",
|
||||
"settings.about.render_backend_format": "Render Backend: {0}",
|
||||
"settings.restart_dialog.title": "Restart required",
|
||||
"settings.restart_dialog.render_mode_message": "Restart the app to switch the rendering mode from \"{0}\" to \"{1}\". Restart now?",
|
||||
"settings.restart_dialog.restart": "Restart now",
|
||||
@@ -314,6 +347,19 @@
|
||||
"settings.plugins.runtime_desc": "Review plugin runtime state and load results.",
|
||||
"settings.plugins.runtime_hint": "This page shows discovery status, load results, and runtime diagnostics for installed plugins.",
|
||||
"settings.plugins.runtime_status": "Plugin runtime status will appear here after plugin discovery completes.",
|
||||
"settings.plugins.description": "Manage installed plugins and browse marketplace packages.",
|
||||
"settings.plugins.initial_status": "Refresh plugin state to see the latest installed and marketplace entries.",
|
||||
"settings.plugins.refresh_button": "Refresh Plugins",
|
||||
"settings.plugins.refresh_success_format": "Loaded {0} installed plugins and {1} marketplace entries.",
|
||||
"settings.plugins.refresh_failed": "Failed to load plugin market index.",
|
||||
"settings.plugins.marketplace_header": "Marketplace",
|
||||
"settings.plugins.marketplace_empty": "No marketplace plugins are available right now.",
|
||||
"settings.plugins.delete_button_short": "Delete",
|
||||
"settings.plugins.install_button_short": "Install",
|
||||
"settings.plugins.restart_required": "Plugin changes take effect after restart.",
|
||||
"settings.plugins.toggle_unchanged_format": "Plugin '{0}' did not change.",
|
||||
"settings.plugins.delete_failed_name_format": "Failed to remove plugin '{0}'.",
|
||||
"settings.plugins.install_failed_name_format": "Failed to install '{0}'.",
|
||||
"settings.plugins.installed_header": "Installed Plugins",
|
||||
"settings.plugins.installed_desc": "Review installed plugins and remove them here.",
|
||||
"settings.plugins.import_header": "Install From Package",
|
||||
@@ -357,6 +403,12 @@
|
||||
"settings.plugin_market.title": "Plugin Market",
|
||||
"settings.plugin_market.subtitle": "Browse plugins from the official LanAirApp source and stage installs.",
|
||||
"settings.plugin_market.unavailable": "Plugin runtime is not available, so the official market cannot be opened right now.",
|
||||
"settings.update.status_idle": "No update check has been performed yet.",
|
||||
"settings.update.status_preferences_saved": "Update preferences saved.",
|
||||
"settings.update.status_check_failed": "Failed to check for updates.",
|
||||
"settings.update.status_available_summary_format": "Update available: {0} (current: {1})",
|
||||
"settings.update.status_up_to_date_format": "You are up to date ({0}).",
|
||||
"settings.window.drawer_default": "Details",
|
||||
"market.toolbar.search_placeholder": "Search plugins",
|
||||
"market.toolbar.refresh": "Refresh",
|
||||
"market.status.loading": "Loading the official plugin market...",
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
{
|
||||
{
|
||||
"app.title": "LanMountainDesktop",
|
||||
"tray.tooltip": "LanMountainDesktop",
|
||||
"tray.menu.show_desktop": "打开桌面",
|
||||
"tray.menu.settings": "设置",
|
||||
"tray.menu.component_library": "独立组件库",
|
||||
"tray.menu.restart": "重启应用",
|
||||
"tray.menu.exit": "退出应用",
|
||||
"button.back_to_windows": "回到Windows",
|
||||
@@ -221,6 +222,32 @@
|
||||
"settings.region.timezone_header": "时区",
|
||||
"settings.region.timezone_desc": "选择时区。时钟与日历组件会使用该时区。",
|
||||
"settings.region.applied_format": "语言已切换为:{0}",
|
||||
"settings.region.follow_system": "跟随系统默认",
|
||||
"settings.general.title": "基本设置",
|
||||
"settings.general.description": "调整语言、时区与运行时行为。",
|
||||
"settings.general.basic_header": "基础设置",
|
||||
"settings.general.runtime_header": "运行设置",
|
||||
"settings.general.preview_header": "日期与时间预览",
|
||||
"settings.general.preview_time_label": "时间",
|
||||
"settings.general.preview_date_label": "日期",
|
||||
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
|
||||
"settings.appearance.title": "外观",
|
||||
"settings.appearance.description": "切换主题、壁纸和状态栏展示。",
|
||||
"settings.appearance.theme_header": "主题",
|
||||
"settings.color.enable_night_mode_toggle": "启用夜间模式",
|
||||
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
|
||||
"settings.color.theme_color_label": "主题强调色",
|
||||
"settings.wallpaper.placement.fill": "填充",
|
||||
"settings.wallpaper.placement.fit": "适应",
|
||||
"settings.wallpaper.placement.stretch": "拉伸",
|
||||
"settings.wallpaper.placement.center": "居中",
|
||||
"settings.wallpaper.placement.tile": "平铺",
|
||||
"settings.status_bar.clock_format_label": "时钟格式",
|
||||
"settings.status_bar.clock_format.hm": "时:分",
|
||||
"settings.status_bar.clock_format.hms": "时:分:秒",
|
||||
"settings.components.title": "组件",
|
||||
"settings.components.description": "调整桌面网格与组件摆放密度。",
|
||||
"settings.components.grid_header": "网格布局",
|
||||
"settings.update.title": "更新",
|
||||
"settings.update.current_version_label": "当前版本",
|
||||
"settings.update.latest_version_label": "最新发布",
|
||||
@@ -273,6 +300,12 @@
|
||||
"settings.about.render_mode.current_format": "当前后端:{0}",
|
||||
"settings.about.render_mode.impl_format": "运行时实现:{0}",
|
||||
"settings.about.render_mode.impl_unavailable": "当前无法获取运行时实现信息。",
|
||||
"settings.about.description": "应用信息与更新偏好。",
|
||||
"settings.about.app_info_header": "应用信息",
|
||||
"settings.about.update_header": "更新",
|
||||
"settings.about.version_label": "版本",
|
||||
"settings.about.render_backend_label": "渲染后端",
|
||||
"settings.about.render_backend_format": "渲染后端:{0}",
|
||||
"settings.restart_dialog.title": "需要重启应用",
|
||||
"settings.restart_dialog.render_mode_message": "需要重启应用,才能将渲染模式从“{0}”切换到“{1}”。是否现在重启?",
|
||||
"settings.restart_dialog.restart": "立即重启",
|
||||
@@ -314,6 +347,19 @@
|
||||
"settings.plugins.runtime_desc": "查看插件运行时状态、加载结果与诊断信息。",
|
||||
"settings.plugins.runtime_hint": "这里展示已安装插件的发现结果、加载状态和运行时诊断信息。",
|
||||
"settings.plugins.runtime_status": "插件扫描完成后,运行时状态会显示在这里。",
|
||||
"settings.plugins.description": "管理已安装插件并浏览插件市场。",
|
||||
"settings.plugins.initial_status": "刷新插件状态以查看最新的已安装插件和市场条目。",
|
||||
"settings.plugins.refresh_button": "刷新插件",
|
||||
"settings.plugins.refresh_success_format": "已加载 {0} 个已安装插件和 {1} 个市场条目。",
|
||||
"settings.plugins.refresh_failed": "加载插件市场索引失败。",
|
||||
"settings.plugins.marketplace_header": "插件市场",
|
||||
"settings.plugins.marketplace_empty": "当前没有可用的市场插件。",
|
||||
"settings.plugins.delete_button_short": "删除",
|
||||
"settings.plugins.install_button_short": "安装",
|
||||
"settings.plugins.restart_required": "插件变更将在重启后生效。",
|
||||
"settings.plugins.toggle_unchanged_format": "插件“{0}”没有变化。",
|
||||
"settings.plugins.delete_failed_name_format": "移除插件“{0}”失败。",
|
||||
"settings.plugins.install_failed_name_format": "安装插件“{0}”失败。",
|
||||
"settings.plugins.installed_header": "已安装插件",
|
||||
"settings.plugins.installed_desc": "在这里查看和删除已安装的插件。",
|
||||
"settings.plugins.import_header": "从安装包导入",
|
||||
@@ -357,6 +403,12 @@
|
||||
"settings.plugin_market.title": "插件市场",
|
||||
"settings.plugin_market.subtitle": "浏览来自 LanAirApp 官方源的插件,并将安装暂存到本地。",
|
||||
"settings.plugin_market.unavailable": "插件运行时不可用,暂时无法打开官方市场。",
|
||||
"settings.update.status_idle": "尚未执行更新检查。",
|
||||
"settings.update.status_preferences_saved": "更新偏好已保存。",
|
||||
"settings.update.status_check_failed": "检查更新失败。",
|
||||
"settings.update.status_available_summary_format": "发现更新:{0}(当前:{1})。",
|
||||
"settings.update.status_up_to_date_format": "当前已是最新版本({0})。",
|
||||
"settings.window.drawer_default": "详情",
|
||||
"market.toolbar.search_placeholder": "搜索插件",
|
||||
"market.toolbar.refresh": "刷新",
|
||||
"market.status.loading": "正在加载官方插件市场...",
|
||||
|
||||
@@ -14,6 +14,8 @@ public sealed class AppSettingsSnapshot
|
||||
|
||||
public string? ThemeColor { get; set; }
|
||||
|
||||
public bool UseSystemChrome { get; set; }
|
||||
|
||||
public string? WallpaperPath { get; set; }
|
||||
|
||||
public string WallpaperPlacement { get; set; } = "Fill";
|
||||
|
||||
@@ -12,6 +12,8 @@ namespace LanMountainDesktop;
|
||||
|
||||
sealed class Program
|
||||
{
|
||||
internal static string StartupRenderMode { get; private set; } = AppRenderingModeHelper.Default;
|
||||
|
||||
// Initialization code. Don't use any Avalonia, third-party APIs or any
|
||||
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
|
||||
// yet and stuff might break.
|
||||
@@ -44,6 +46,7 @@ sealed class Program
|
||||
try
|
||||
{
|
||||
var renderMode = LoadConfiguredRenderMode();
|
||||
StartupRenderMode = renderMode;
|
||||
AppLogger.Info("Startup", $"Resolved render mode '{renderMode}'.");
|
||||
App.CurrentSingleInstanceService = singleInstance;
|
||||
BuildAvaloniaApp(renderMode).StartWithClassicDesktopLifetime(args);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
41
LanMountainDesktop/Services/IComponentLibraryService.cs
Normal file
41
LanMountainDesktop/Services/IComponentLibraryService.cs
Normal 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);
|
||||
}
|
||||
@@ -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)
|
||||
]);
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
334
LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs
Normal file
334
LanMountainDesktop/Services/Settings/SettingsPageRegistry.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
306
LanMountainDesktop/Services/Settings/SettingsWindowService.cs
Normal file
306
LanMountainDesktop/Services/Settings/SettingsWindowService.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -53,6 +53,20 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="NumericUpDown">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|NumberBox">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
@@ -60,11 +74,15 @@
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="CheckBox">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleSwitch">
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope Border.settings-expander-shell">
|
||||
<Style Selector=".settings-scope Border.settings-expander-shell">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
@@ -73,28 +91,38 @@
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope Button">
|
||||
<Style Selector=".settings-scope Button">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBox">
|
||||
<Style Selector=".settings-scope ComboBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBoxItem">
|
||||
<Style Selector=".settings-scope TextBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope ComboBoxItem">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusXs}" />
|
||||
<Setter Property="Padding" Value="10,6" />
|
||||
<Setter Property="Margin" Value="4,2" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ui|NumberBox">
|
||||
<Style Selector=".settings-scope ui|NumberBox">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton">
|
||||
<Style Selector=".settings-scope NumericUpDown">
|
||||
<Setter Property="CornerRadius" Value="{DynamicResource DesignCornerRadiusSm}" />
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-scope RadioButton">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
@@ -103,11 +131,11 @@
|
||||
<Setter Property="MinHeight" Value="34" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton:pointerover">
|
||||
<Style Selector=".settings-scope RadioButton:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope RadioButton:checked">
|
||||
<Style Selector=".settings-scope RadioButton:checked">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBox">
|
||||
<Style Selector=".settings-scope ComboBox">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
|
||||
@@ -80,11 +80,11 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ComboBox:pointerover">
|
||||
<Style Selector=".settings-scope ComboBox:pointerover">
|
||||
<Setter Property="RenderTransform" Value="scale(1.01)" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ToggleSwitch">
|
||||
<Style Selector=".settings-scope ToggleSwitch">
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="{StaticResource FluttermotionToken.Duration.Standard}" Easing="0.22,1,0.36,1" />
|
||||
@@ -93,7 +93,7 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-scope ToggleSwitch:pointerover">
|
||||
<Style Selector=".settings-scope ToggleSwitch:pointerover">
|
||||
<Setter Property="RenderTransform" Value="scale(1.01)" />
|
||||
</Style>
|
||||
</Styles>
|
||||
|
||||
198
LanMountainDesktop/Styles/SettingsCardStyles.axaml
Normal file
198
LanMountainDesktop/Styles/SettingsCardStyles.axaml
Normal file
@@ -0,0 +1,198 @@
|
||||
<Styles xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls">
|
||||
|
||||
<Style Selector="StackPanel.settings-page-container">
|
||||
<Setter Property="Spacing" Value="0" />
|
||||
<Setter Property="Margin" Value="12,14,28,32" />
|
||||
<Setter Property="MaxWidth" Value="900" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-section-title">
|
||||
<Setter Property="FontSize" Value="30" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-section-description">
|
||||
<Setter Property="FontSize" Value="14" />
|
||||
<Setter Property="Opacity" Value="0.76" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="MaxWidth" Value="760" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,22" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-subsection-title">
|
||||
<Setter Property="FontSize" Value="15" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,18,0,10" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-section-card, Border.settings-option-card, Border.settings-list-item">
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<BrushTransition Property="Background"
|
||||
Duration="{StaticResource FluttermotionToken.Duration.Standard}"
|
||||
Easing="0.22,1,0.36,1" />
|
||||
<BoxShadowsTransition Property="BoxShadow"
|
||||
Duration="{StaticResource FluttermotionToken.Duration.Fast}"
|
||||
Easing="0.22,1,0.36,1" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-section-card">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="18" />
|
||||
<Setter Property="Padding" Value="20" />
|
||||
<Setter Property="Margin" Value="0,0,0,14" />
|
||||
<Setter Property="BoxShadow" Value="0 2 8 #12000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-option-card">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
<Setter Property="Margin" Value="0,0,0,12" />
|
||||
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-list-item">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
<Setter Property="Margin" Value="0,0,0,10" />
|
||||
<Setter Property="BoxShadow" Value="0 1 4 #0F000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-list-item:pointerover, Border.settings-option-card:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BoxShadow" Value="0 4 10 #13000000" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-option-card-icon-host, Border.settings-section-card-icon-host">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="HorizontalAlignment" Value="Left" />
|
||||
<Setter Property="VerticalAlignment" Value="Top" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-option-card-icon-host">
|
||||
<Setter Property="Width" Value="36" />
|
||||
<Setter Property="Height" Value="36" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.settings-section-card-icon-host">
|
||||
<Setter Property="Width" Value="42" />
|
||||
<Setter Property="Height" Value="42" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-card-header">
|
||||
<Setter Property="FontSize" Value="17" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,12" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-card-description">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="Opacity" Value="0.72" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="Margin" Value="0,-8,0,0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-item-label">
|
||||
<Setter Property="FontSize" Value="13" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.settings-item-description">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Opacity" Value="0.68" />
|
||||
<Setter Property="TextWrapping" Value="Wrap" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||
<Setter Property="MaxWidth" Value="620" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="StackPanel.settings-item">
|
||||
<Setter Property="Spacing" Value="8" />
|
||||
<Setter Property="Margin" Value="0,0,0,16" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-inline-pair">
|
||||
<Setter Property="ColumnSpacing" Value="14" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Grid.settings-list-actions">
|
||||
<Setter Property="ColumnSpacing" Value="12" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card">
|
||||
<Setter Property="Margin" Value="0,0,0,14" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#FooterContentPresenter">
|
||||
<Setter Property="Margin" Value="0,6,0,2" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card /template/ ContentPresenter#ContentPresenter">
|
||||
<Setter Property="Margin" Value="0,14,0,0" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card ComboBox, .settings-section-card ComboBox, .settings-option-card ComboBox">
|
||||
<Setter Property="MinWidth" Value="220" />
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card TextBox, .settings-section-card TextBox, .settings-option-card TextBox">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="38" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card NumericUpDown, .settings-section-card NumericUpDown, .settings-option-card NumericUpDown">
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch" />
|
||||
<Setter Property="MinHeight" Value="38" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ui|SettingsExpander.settings-expander-card ToggleSwitch, .settings-option-card ToggleSwitch, .settings-list-item ToggleSwitch">
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="OnContent" Value="{x:Null}" />
|
||||
<Setter Property="OffContent" Value="{x:Null}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector=".settings-section-card Button, .settings-option-card Button, .settings-list-item Button, ui|SettingsExpander.settings-expander-card Button">
|
||||
<Setter Property="MinHeight" Value="36" />
|
||||
<Setter Property="Padding" Value="14,8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.settings-accent-button">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}" />
|
||||
<Setter Property="Foreground" Value="{DynamicResource AdaptiveOnAccentBrush}" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="16,10" />
|
||||
<Setter Property="MinHeight" Value="36" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.settings-accent-button:pointerover">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentLightBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Separator.settings-separator">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<Setter Property="Margin" Value="0,18" />
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Opacity" Value="0.5" />
|
||||
</Style>
|
||||
|
||||
</Styles>
|
||||
@@ -0,0 +1,57 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using Avalonia.Controls;
|
||||
using FluentIcons.Common;
|
||||
|
||||
namespace LanMountainDesktop.ViewModels;
|
||||
|
||||
public sealed class ComponentLibraryWindowViewModel : ViewModelBase
|
||||
{
|
||||
public string Title { get; set; } = "Widgets";
|
||||
|
||||
public ObservableCollection<ComponentLibraryCategoryViewModel> Categories { get; } = [];
|
||||
|
||||
public ObservableCollection<ComponentLibraryItemViewModel> Components { get; } = [];
|
||||
}
|
||||
|
||||
public sealed class ComponentLibraryCategoryViewModel
|
||||
{
|
||||
public ComponentLibraryCategoryViewModel(
|
||||
string id,
|
||||
string title,
|
||||
Symbol icon,
|
||||
IReadOnlyList<ComponentLibraryItemViewModel> components)
|
||||
{
|
||||
Id = id;
|
||||
Title = title;
|
||||
Icon = icon;
|
||||
Components = components;
|
||||
}
|
||||
|
||||
public string Id { get; }
|
||||
|
||||
public string Title { get; }
|
||||
|
||||
public Symbol Icon { get; }
|
||||
|
||||
public IReadOnlyList<ComponentLibraryItemViewModel> Components { get; }
|
||||
}
|
||||
|
||||
public sealed class ComponentLibraryItemViewModel
|
||||
{
|
||||
public ComponentLibraryItemViewModel(
|
||||
string componentId,
|
||||
string displayName,
|
||||
Control? previewControl)
|
||||
{
|
||||
ComponentId = componentId;
|
||||
DisplayName = displayName;
|
||||
PreviewControl = previewControl;
|
||||
}
|
||||
|
||||
public string ComponentId { get; }
|
||||
|
||||
public string DisplayName { get; }
|
||||
|
||||
public Control? PreviewControl { get; }
|
||||
}
|
||||
1299
LanMountainDesktop/ViewModels/SettingsViewModels.cs
Normal file
1299
LanMountainDesktop/ViewModels/SettingsViewModels.cs
Normal file
File diff suppressed because it is too large
Load Diff
129
LanMountainDesktop/Views/ComponentLibraryWindow.axaml
Normal file
129
LanMountainDesktop/Views/ComponentLibraryWindow.axaml
Normal file
@@ -0,0 +1,129 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.ComponentLibraryWindow"
|
||||
x:DataType="vm:ComponentLibraryWindowViewModel"
|
||||
Width="980"
|
||||
Height="620"
|
||||
MinWidth="760"
|
||||
MinHeight="500"
|
||||
CanResize="True"
|
||||
SystemDecorations="Full"
|
||||
Title="Component Library"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
|
||||
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="10"
|
||||
Margin="14">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
Margin="6,4,6,0">
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding Title}" />
|
||||
<Button Grid.Column="1"
|
||||
Width="34"
|
||||
Height="34"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseClick">
|
||||
<fi:FluentIcon Icon="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1"
|
||||
ColumnDefinitions="240,*"
|
||||
ColumnSpacing="12">
|
||||
<Border Classes="glass-panel"
|
||||
CornerRadius="24"
|
||||
Padding="10">
|
||||
<ListBox x:Name="CategoryListBox"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
SelectionChanged="OnCategorySelectionChanged"
|
||||
ItemsSource="{Binding Categories}">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ComponentLibraryCategoryViewModel">
|
||||
<Border Padding="10"
|
||||
Margin="0,0,0,6"
|
||||
CornerRadius="14"
|
||||
Background="{DynamicResource AdaptiveNavItemBackgroundBrush}">
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
<fi:SymbolIcon Symbol="{Binding Icon}"
|
||||
IconVariant="Regular"
|
||||
FontSize="16" />
|
||||
<TextBlock Grid.Column="1"
|
||||
VerticalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding Title}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<Border Grid.Column="1"
|
||||
Classes="glass-strong"
|
||||
CornerRadius="24"
|
||||
Padding="10">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<ItemsControl ItemsSource="{Binding Components}">
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel Orientation="Horizontal" />
|
||||
</ItemsPanelTemplate>
|
||||
</ItemsControl.ItemsPanel>
|
||||
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ComponentLibraryItemViewModel">
|
||||
<Border Width="240"
|
||||
Height="220"
|
||||
Margin="6"
|
||||
CornerRadius="18"
|
||||
Padding="10"
|
||||
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
|
||||
BorderThickness="1">
|
||||
<Grid RowDefinitions="*,Auto,Auto"
|
||||
RowSpacing="8">
|
||||
<Border CornerRadius="12"
|
||||
Background="{DynamicResource AdaptiveGlassPanelBackgroundBrush}"
|
||||
BorderThickness="1"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
Padding="8">
|
||||
<ContentControl HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Content="{Binding PreviewControl}" />
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="{Binding DisplayName}" />
|
||||
|
||||
<Button Grid.Row="2"
|
||||
HorizontalAlignment="Center"
|
||||
Padding="12,6"
|
||||
Tag="{Binding ComponentId}"
|
||||
Click="OnAddComponentClick">
|
||||
<TextBlock Text="Add to Desktop" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
237
LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs
Normal file
237
LanMountainDesktop/Views/ComponentLibraryWindow.axaml.cs
Normal file
@@ -0,0 +1,237 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using FluentIcons.Common;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class ComponentLibraryWindow : Window
|
||||
{
|
||||
private IComponentLibraryService? _componentLibraryService;
|
||||
private Func<double, ComponentLibraryCreateContext>? _createContextFactory;
|
||||
private Func<string, string, string>? _localize;
|
||||
private readonly ComponentLibraryWindowViewModel _viewModel = new();
|
||||
|
||||
public ComponentLibraryWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContext = _viewModel;
|
||||
}
|
||||
|
||||
public ComponentLibraryWindow(
|
||||
IComponentLibraryService componentLibraryService,
|
||||
Func<double, ComponentLibraryCreateContext> createContextFactory,
|
||||
Func<string, string, string> localize)
|
||||
: this()
|
||||
{
|
||||
_componentLibraryService = componentLibraryService ?? throw new ArgumentNullException(nameof(componentLibraryService));
|
||||
_createContextFactory = createContextFactory ?? throw new ArgumentNullException(nameof(createContextFactory));
|
||||
_localize = localize ?? throw new ArgumentNullException(nameof(localize));
|
||||
Reload();
|
||||
}
|
||||
|
||||
public event EventHandler<string>? AddComponentRequested;
|
||||
|
||||
public void Reload()
|
||||
{
|
||||
if (_componentLibraryService is null ||
|
||||
_createContextFactory is null ||
|
||||
_localize is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_viewModel.Title = _localize("component_library.title", "Widgets");
|
||||
_viewModel.Categories.Clear();
|
||||
_viewModel.Components.Clear();
|
||||
|
||||
var categories = _componentLibraryService.GetDesktopCategories();
|
||||
foreach (var category in categories)
|
||||
{
|
||||
var itemModels = category.Components
|
||||
.Select(CreateComponentItem)
|
||||
.ToArray();
|
||||
_viewModel.Categories.Add(new ComponentLibraryCategoryViewModel(
|
||||
category.Id,
|
||||
GetLocalizedCategoryTitle(category.Id),
|
||||
ResolveCategoryIcon(category.Id),
|
||||
itemModels));
|
||||
}
|
||||
|
||||
if (_viewModel.Categories.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (CategoryListBox is not null)
|
||||
{
|
||||
CategoryListBox.SelectedIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private ComponentLibraryItemViewModel CreateComponentItem(ComponentLibraryComponentEntry entry)
|
||||
{
|
||||
if (_componentLibraryService is null ||
|
||||
_createContextFactory is null ||
|
||||
_localize is null)
|
||||
{
|
||||
return new ComponentLibraryItemViewModel(entry.ComponentId, entry.DisplayName, previewControl: null);
|
||||
}
|
||||
|
||||
Control? previewControl = null;
|
||||
_componentLibraryService.TryCreateControl(
|
||||
entry.ComponentId,
|
||||
_createContextFactory(42),
|
||||
out previewControl,
|
||||
out _);
|
||||
|
||||
if (previewControl is not null)
|
||||
{
|
||||
previewControl.IsHitTestVisible = false;
|
||||
previewControl.Focusable = false;
|
||||
}
|
||||
|
||||
return new ComponentLibraryItemViewModel(
|
||||
entry.ComponentId,
|
||||
string.IsNullOrWhiteSpace(entry.DisplayNameLocalizationKey)
|
||||
? entry.DisplayName
|
||||
: _localize(entry.DisplayNameLocalizationKey, entry.DisplayName),
|
||||
previewControl);
|
||||
}
|
||||
|
||||
private void OnCategorySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_viewModel.Components.Clear();
|
||||
|
||||
if (CategoryListBox?.SelectedItem is not ComponentLibraryCategoryViewModel selectedCategory)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var component in selectedCategory.Components)
|
||||
{
|
||||
_viewModel.Components.Add(component);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAddComponentClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
if (sender is not Button button ||
|
||||
button.Tag is not string componentId ||
|
||||
string.IsNullOrWhiteSpace(componentId))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
AddComponentRequested?.Invoke(this, componentId);
|
||||
}
|
||||
|
||||
private void OnCloseClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
Hide();
|
||||
}
|
||||
|
||||
private Symbol ResolveCategoryIcon(string categoryId)
|
||||
{
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Clock;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.CalendarDate;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.WeatherSunny;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Edit;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Play;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Calculator;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
return Symbol.Apps;
|
||||
}
|
||||
|
||||
private string GetLocalizedCategoryTitle(string categoryId)
|
||||
{
|
||||
if (_localize is null)
|
||||
{
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Clock", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.clock", "Clock");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Date", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.date", "Calendar");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Weather", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.weather", "Weather");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Board", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.board", "Board");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Media", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.media", "Media");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Info", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.info", "Info");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Calculator", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.calculator", "Calculator");
|
||||
}
|
||||
|
||||
if (string.Equals(categoryId, "Study", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return _localize("component_category.study", "Study");
|
||||
}
|
||||
|
||||
return categoryId;
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,8 @@ using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
@@ -59,7 +61,7 @@ public partial class AnalogClockWidget : UserControl, IDesktopComponentWidget, I
|
||||
private string _componentId = BuiltInComponentIds.DesktopClock;
|
||||
private string _placementId = string.Empty;
|
||||
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private TimeZoneService? _timeZoneService;
|
||||
private double _currentCellSize = 48;
|
||||
|
||||
@@ -35,8 +35,8 @@ public partial class BaiduHotSearchWidget : UserControl, IDesktopComponentWidget
|
||||
Interval = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<BaiduHotSearchItemSnapshot> _activeItems = [];
|
||||
private readonly List<HotItemVisual> _hotItemVisuals = [];
|
||||
|
||||
@@ -33,8 +33,8 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
|
||||
Interval = TimeSpan.FromMinutes(15)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<BilibiliHotSearchItemSnapshot> _activeItems = [];
|
||||
private readonly List<HotItemVisual> _hotItemVisuals = [];
|
||||
|
||||
@@ -336,8 +336,6 @@ public partial class BrowserWidget : UserControl, IDesktopComponentWidget,
|
||||
GoButton.IsEnabled = true;
|
||||
AddressTextBox.IsEnabled = true;
|
||||
UnavailableOverlay.IsVisible = false;
|
||||
|
||||
TryNavigate(_lastKnownUri, "Activate");
|
||||
}
|
||||
|
||||
private void DeactivateWebView(bool clearUrl)
|
||||
|
||||
@@ -9,6 +9,7 @@ using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
@@ -27,7 +28,7 @@ public partial class ClassScheduleWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromMinutes(4)
|
||||
};
|
||||
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly IClassIslandScheduleDataService _scheduleService = new ClassIslandScheduleDataService();
|
||||
|
||||
|
||||
@@ -44,8 +44,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromMinutes(30)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Bitmap?[] _newsBitmaps = new Bitmap?[2];
|
||||
private readonly List<string?> _newsUrls = [];
|
||||
@@ -875,4 +875,3 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
||||
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
|
||||
@@ -59,7 +60,7 @@ public partial class DailyArtworkWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
|
||||
@@ -55,7 +55,7 @@ public partial class DailyPoetryWidget : UserControl, IDesktopComponentWidget, I
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
|
||||
@@ -33,8 +33,8 @@ public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
|
||||
@@ -31,8 +31,8 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
@@ -16,7 +17,9 @@ public sealed record DesktopComponentControlFactoryContext(
|
||||
IWeatherInfoService WeatherInfoService,
|
||||
IRecommendationInfoService RecommendationInfoService,
|
||||
ICalculatorDataService CalculatorDataService,
|
||||
ISettingsFacadeService SettingsFacade,
|
||||
ISettingsService SettingsService,
|
||||
IComponentInstanceSettingsStore ComponentSettingsStore,
|
||||
IComponentSettingsAccessor ComponentSettingsAccessor,
|
||||
string? PlacementId = null);
|
||||
|
||||
@@ -87,10 +90,15 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
IWeatherInfoService weatherInfoService,
|
||||
IRecommendationInfoService recommendationInfoService,
|
||||
ICalculatorDataService calculatorDataService,
|
||||
ISettingsFacadeService settingsFacade,
|
||||
string? placementId = null)
|
||||
{
|
||||
var settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
ArgumentNullException.ThrowIfNull(settingsFacade);
|
||||
|
||||
var settingsService = settingsFacade.Settings;
|
||||
var componentAccessor = settingsService.GetComponentAccessor(Definition.Id, placementId);
|
||||
var componentSettingsStore = new ComponentSettingsService(settingsService);
|
||||
componentSettingsStore.SetScopedComponentContext(Definition.Id, placementId);
|
||||
var control = _controlFactory(new DesktopComponentControlFactoryContext(
|
||||
Definition,
|
||||
cellSize,
|
||||
@@ -98,20 +106,37 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
weatherInfoService,
|
||||
recommendationInfoService,
|
||||
calculatorDataService,
|
||||
settingsFacade,
|
||||
settingsService,
|
||||
componentSettingsStore,
|
||||
componentAccessor,
|
||||
placementId));
|
||||
var runtimeContext = new DesktopComponentRuntimeContext(
|
||||
Definition.Id,
|
||||
placementId,
|
||||
settingsFacade,
|
||||
settingsService,
|
||||
componentAccessor);
|
||||
componentAccessor,
|
||||
componentSettingsStore);
|
||||
|
||||
ApplySettingsDependencies(control, settingsService, componentSettingsStore);
|
||||
|
||||
if (control is IComponentRuntimeContextAware runtimeContextAwareComponent)
|
||||
{
|
||||
runtimeContextAwareComponent.SetComponentRuntimeContext(runtimeContext);
|
||||
}
|
||||
|
||||
if (control is IComponentSettingsContextAware settingsContextAwareComponent)
|
||||
{
|
||||
settingsContextAwareComponent.SetComponentSettingsContext(new DesktopComponentSettingsContext(
|
||||
Definition.Id,
|
||||
placementId,
|
||||
settingsFacade,
|
||||
settingsService,
|
||||
componentAccessor,
|
||||
componentSettingsStore));
|
||||
}
|
||||
|
||||
if (control is IComponentPlacementContextAware placementAwareComponent)
|
||||
{
|
||||
placementAwareComponent.SetComponentPlacementContext(Definition.Id, placementId);
|
||||
@@ -149,6 +174,57 @@ public sealed class DesktopComponentRuntimeDescriptor
|
||||
{
|
||||
return _cornerRadiusResolver(Math.Max(1, cellSize));
|
||||
}
|
||||
|
||||
private static void ApplySettingsDependencies(
|
||||
object? target,
|
||||
ISettingsService settingsService,
|
||||
IComponentInstanceSettingsStore componentSettingsStore)
|
||||
{
|
||||
if (target is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var flags = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
|
||||
|
||||
foreach (var field in target.GetType().GetFields(flags))
|
||||
{
|
||||
if (field.IsInitOnly)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof(ISettingsService).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
field.SetValue(target, settingsService);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof(IComponentInstanceSettingsStore).IsAssignableFrom(field.FieldType))
|
||||
{
|
||||
field.SetValue(target, componentSettingsStore);
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var property in target.GetType().GetProperties(flags))
|
||||
{
|
||||
if (!property.CanWrite)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof(ISettingsService).IsAssignableFrom(property.PropertyType))
|
||||
{
|
||||
property.SetValue(target, settingsService);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (typeof(IComponentInstanceSettingsStore).IsAssignableFrom(property.PropertyType))
|
||||
{
|
||||
property.SetValue(target, componentSettingsStore);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class DesktopComponentRuntimeRegistry
|
||||
@@ -368,8 +444,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
];
|
||||
}
|
||||
|
||||
public static DesktopComponentRuntimeRegistry CreateDefault(ComponentRegistry componentRegistry)
|
||||
public static DesktopComponentRuntimeRegistry CreateDefault(
|
||||
ComponentRegistry componentRegistry,
|
||||
ISettingsFacadeService settingsFacade)
|
||||
{
|
||||
_ = settingsFacade;
|
||||
return new DesktopComponentRuntimeRegistry(componentRegistry, GetDefaultRegistrations());
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ public partial class ExchangeRateCalculatorWidget : UserControl, IDesktopCompone
|
||||
Interval = TimeSpan.FromMinutes(30)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
private ICalculatorDataService _calculatorDataService = DefaultCalculatorService;
|
||||
|
||||
@@ -11,6 +11,7 @@ using Avalonia.Media;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
@@ -26,7 +27,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
|
||||
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
|
||||
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
|
||||
private readonly ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private ISettingsService _settingsService = HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
|
||||
|
||||
@@ -95,8 +95,8 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
|
||||
@@ -44,8 +44,8 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
|
||||
Interval = TimeSpan.FromMinutes(20)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<DailyNewsItemSnapshot> _activeItems = [];
|
||||
private readonly List<NewsItemVisual> _itemVisuals = [];
|
||||
|
||||
@@ -93,8 +93,8 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
|
||||
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
|
||||
@@ -29,7 +29,7 @@ public partial class MusicControlWidget : UserControl, IDesktopComponentWidget,
|
||||
|
||||
private readonly IMusicControlService _musicControlService = MusicControlServiceFactory.CreateDefault();
|
||||
private readonly MonetColorService _monetColorService = new();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private CancellationTokenSource? _refreshCts;
|
||||
|
||||
@@ -26,7 +26,7 @@ public partial class RecordingWidget : UserControl, IDesktopComponentWidget, IDe
|
||||
|
||||
private readonly IAudioRecorderService _audioRecorderService = AudioRecorderServiceFactory.CreateRecorder();
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<Border> _waveBars = [];
|
||||
private readonly double[] _waveLevels = new double[WaveBarCount];
|
||||
|
||||
@@ -45,8 +45,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
Interval = TimeSpan.FromMinutes(20)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly List<Stcn24ForumPostItemSnapshot> _activeItems = [];
|
||||
private readonly List<ForumItemVisual> _itemVisuals = [];
|
||||
|
||||
@@ -41,7 +41,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
private static readonly Color LightSubstrate = Color.Parse("#FFF1F5FA");
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -73,6 +73,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyVariableFontFamily();
|
||||
ReloadLanguageCode();
|
||||
@@ -114,6 +115,11 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -572,7 +578,7 @@ public partial class StudyDeductionReasonsWidget : UserControl, IDesktopComponen
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
|
||||
@@ -13,8 +13,8 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
{
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -38,6 +38,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ReloadDisplaySettings();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
@@ -106,6 +107,11 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -330,7 +336,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
|
||||
private IBrush TryResolveThemeBrush(string resourceKey, string fallbackHex)
|
||||
{
|
||||
if (Resources.TryGetResource(resourceKey, ActualThemeVariant, out var resource) && resource is IBrush brush)
|
||||
if (this.TryFindResource(resourceKey, out var resource) && resource is IBrush brush)
|
||||
{
|
||||
return brush;
|
||||
}
|
||||
@@ -352,6 +358,7 @@ public partial class StudyEnvironmentWidget : UserControl, IDesktopComponentWidg
|
||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
SizeChanged -= OnSizeChanged;
|
||||
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||
|
||||
_monitoringLease?.Dispose();
|
||||
_monitoringLease = null;
|
||||
|
||||
@@ -41,7 +41,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -79,6 +79,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyVariableFontFamily();
|
||||
ReloadLanguageCode();
|
||||
@@ -123,6 +124,11 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -506,7 +512,7 @@ public partial class StudyInterruptDensityWidget : UserControl, IDesktopComponen
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
|
||||
@@ -55,7 +55,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
private readonly object _snapshotSync = new();
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _renderTimer = new()
|
||||
{
|
||||
@@ -89,6 +89,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ReloadLanguageCode();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
@@ -192,6 +193,24 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
ApplyTypographyByBackground(panelColor);
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
var panelColor = ResolvePanelBackgroundColor();
|
||||
ApplyTypographyByBackground(panelColor);
|
||||
ApplyStatusBadgeStyle(StatusVisualKind.Default, panelColor);
|
||||
|
||||
lock (_snapshotSync)
|
||||
{
|
||||
_pendingSnapshot = _studyAnalyticsService.GetSnapshot();
|
||||
_hasPendingSnapshot = true;
|
||||
}
|
||||
|
||||
if (!_renderTimer.IsEnabled)
|
||||
{
|
||||
OnRenderTimerTick(this, EventArgs.Empty);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStudySnapshotUpdated(object? sender, StudyAnalyticsSnapshotChangedEventArgs e)
|
||||
{
|
||||
lock (_snapshotSync)
|
||||
@@ -375,7 +394,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
@@ -580,6 +599,7 @@ public partial class StudyNoiseCurveWidget : UserControl, IDesktopComponentWidge
|
||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
SizeChanged -= OnSizeChanged;
|
||||
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||
|
||||
if (_isSubscribed)
|
||||
{
|
||||
|
||||
@@ -42,7 +42,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -75,6 +75,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyVariableFontFamily();
|
||||
ReloadLanguageCode();
|
||||
@@ -129,6 +130,11 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -396,7 +402,7 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
@@ -627,10 +633,9 @@ public partial class StudyNoiseDistributionWidget : UserControl, IDesktopCompone
|
||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
SizeChanged -= OnSizeChanged;
|
||||
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||
|
||||
_monitoringLease?.Dispose();
|
||||
_monitoringLease = null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -68,6 +68,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyVariableFontFamily();
|
||||
ReloadLanguageCode();
|
||||
@@ -112,6 +113,11 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -501,7 +507,7 @@ public partial class StudyScoreOverviewWidget : UserControl, IDesktopComponentWi
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
|
||||
@@ -50,7 +50,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly StudyAnalyticsMonitoringLeaseCoordinator _monitoringLeaseCoordinator = StudyAnalyticsMonitoringLeaseCoordinatorFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly DispatcherTimer _uiTimer = new()
|
||||
{
|
||||
@@ -76,6 +76,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ReloadLanguageCode();
|
||||
ApplyCellSize(_currentCellSize);
|
||||
@@ -119,6 +120,11 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
ApplyTypographyByBackground(ResolvePanelBackgroundColor());
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
}
|
||||
|
||||
private void OnUiTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
RefreshVisual();
|
||||
@@ -334,7 +340,7 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
@@ -484,5 +490,6 @@ public partial class StudySessionControlWidget : UserControl, IDesktopComponentW
|
||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
SizeChanged -= OnSizeChanged;
|
||||
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
private static readonly Color LightSubstrate = Color.Parse("#FFF1F5FA");
|
||||
|
||||
private readonly IStudyAnalyticsService _studyAnalyticsService = StudyAnalyticsServiceFactory.CreateDefault();
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private double _currentCellSize = 48;
|
||||
@@ -72,6 +72,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
DialogCancelButton.Click += (_, _) => CloseDialog();
|
||||
DialogConfirmButton.Click += (_, _) => ConfirmDialog();
|
||||
DialogRenameTextBox.KeyDown += OnDialogRenameTextBoxKeyDown;
|
||||
@@ -133,6 +134,14 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
}
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_currentSnapshot is not null)
|
||||
{
|
||||
RenderSnapshot(_currentSnapshot);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnStudySnapshotUpdated(object? sender, StudyAnalyticsSnapshotChangedEventArgs e)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
@@ -657,7 +666,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
return solidBackground.Color;
|
||||
}
|
||||
|
||||
if (Resources.TryGetResource("AdaptiveGlassStrongBackgroundBrush", ActualThemeVariant, out var resource) &&
|
||||
if (this.TryFindResource("AdaptiveGlassStrongBackgroundBrush", out var resource) &&
|
||||
resource is ISolidColorBrush solidBrush)
|
||||
{
|
||||
return solidBrush.Color;
|
||||
@@ -747,6 +756,7 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
AttachedToVisualTree -= OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree -= OnDetachedFromVisualTree;
|
||||
SizeChanged -= OnSizeChanged;
|
||||
ActualThemeVariantChanged -= OnActualThemeVariantChanged;
|
||||
DialogCancelButton.Click -= (_, _) => CloseDialog();
|
||||
DialogConfirmButton.Click -= (_, _) => ConfirmDialog();
|
||||
DialogRenameTextBox.KeyDown -= OnDialogRenameTextBoxKeyDown;
|
||||
@@ -758,5 +768,3 @@ public partial class StudySessionHistoryWidget : UserControl, IDesktopComponentW
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -41,8 +41,8 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
|
||||
Interval = TimeSpan.FromMinutes(12)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
|
||||
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);
|
||||
|
||||
@@ -89,8 +89,8 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
|
||||
Interval = FluttermotionToken.WeatherAnimationFrameInterval
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _settingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _settingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
|
||||
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
|
||||
|
||||
@@ -92,8 +92,8 @@ public partial class WorldClockWidget : UserControl, IDesktopComponentWidget, IT
|
||||
Interval = TimeSpan.FromSeconds(1)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
|
||||
private IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly ClockEntryVisual[] _entryVisuals = new ClockEntryVisual[WorldClockTimeZoneCatalog.ClockCount];
|
||||
private readonly TimeZoneInfo[] _entryTimeZones = new TimeZoneInfo[WorldClockTimeZoneCatalog.ClockCount];
|
||||
|
||||
@@ -15,6 +15,7 @@ using FluentIcons.Common;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.Theme;
|
||||
using LanMountainDesktop.Views.Components;
|
||||
|
||||
@@ -44,7 +45,7 @@ public partial class MainWindow
|
||||
private TranslateTransform? _componentLibraryCategoryHostTransform;
|
||||
private TranslateTransform? _componentLibraryComponentHostTransform;
|
||||
private IReadOnlyList<ComponentLibraryCategory> _componentLibraryCategories = Array.Empty<ComponentLibraryCategory>();
|
||||
private IReadOnlyList<DesktopComponentRuntimeDescriptor> _componentLibraryActiveComponents = Array.Empty<DesktopComponentRuntimeDescriptor>();
|
||||
private IReadOnlyList<ComponentLibraryComponentEntry> _componentLibraryActiveComponents = Array.Empty<ComponentLibraryComponentEntry>();
|
||||
private bool _isComponentLibraryCategoryGestureActive;
|
||||
private bool _isComponentLibraryComponentGestureActive;
|
||||
private Point _componentLibraryCategoryGestureStartPoint;
|
||||
@@ -95,13 +96,51 @@ public partial class MainWindow
|
||||
string Id,
|
||||
Symbol Icon,
|
||||
string Title,
|
||||
IReadOnlyList<DesktopComponentRuntimeDescriptor> Components);
|
||||
IReadOnlyList<ComponentLibraryComponentEntry> Components);
|
||||
|
||||
private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale);
|
||||
|
||||
private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_componentLibraryWindowService.Toggle(this);
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (_isComponentLibraryOpen)
|
||||
{
|
||||
CloseComponentLibraryWindow(reopenSettings: false);
|
||||
return;
|
||||
}
|
||||
|
||||
var settingsWindowService = (Application.Current as App)?.SettingsWindowService;
|
||||
_reopenSettingsAfterComponentLibraryClose = settingsWindowService?.IsOpen == true;
|
||||
if (_reopenSettingsAfterComponentLibraryClose)
|
||||
{
|
||||
settingsWindowService?.Close();
|
||||
}
|
||||
|
||||
OpenComponentLibraryWindow();
|
||||
}
|
||||
|
||||
private void OnOpenSettingsClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
|
||||
if (_isComponentLibraryOpen)
|
||||
{
|
||||
CloseComponentLibraryWindow(reopenSettings: false);
|
||||
}
|
||||
|
||||
var app = Application.Current as App;
|
||||
if (app?.SettingsWindowService is { } settingsWindowService)
|
||||
{
|
||||
settingsWindowService.Toggle(new SettingsWindowOpenRequest(
|
||||
Source: "MainWindowTaskbar",
|
||||
Owner: this));
|
||||
return;
|
||||
}
|
||||
|
||||
app?.OpenIndependentSettingsModule("MainWindowTaskbar");
|
||||
}
|
||||
|
||||
private void OnCloseComponentLibraryClick(object? sender, RoutedEventArgs e)
|
||||
@@ -220,16 +259,19 @@ public partial class MainWindow
|
||||
|
||||
private void ApplyTaskbarActionVisibility(TaskbarContext context)
|
||||
{
|
||||
if (BackToWindowsButton is null || OpenComponentLibraryButton is null)
|
||||
if (BackToWindowsButton is null ||
|
||||
OpenComponentLibraryButton is null ||
|
||||
OpenSettingsButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows);
|
||||
var showSettings = false;
|
||||
var showDesktopEdit = true;
|
||||
var showSettings = true;
|
||||
var showDesktopEdit = _isSettingsOpen;
|
||||
|
||||
BackToWindowsButton.IsVisible = showMinimize;
|
||||
OpenSettingsButton.IsVisible = showSettings;
|
||||
OpenComponentLibraryButton.IsVisible = showDesktopEdit;
|
||||
|
||||
if (TaskbarFixedActionsHost is not null)
|
||||
@@ -242,6 +284,8 @@ public partial class MainWindow
|
||||
TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit;
|
||||
}
|
||||
|
||||
UpdateOpenSettingsActionVisualState();
|
||||
|
||||
var dynamicActions = ResolveDynamicTaskbarActions(context)
|
||||
.Where(action => action.IsVisible)
|
||||
.ToList();
|
||||
@@ -256,7 +300,25 @@ public partial class MainWindow
|
||||
|
||||
private void UpdateOpenSettingsActionVisualState()
|
||||
{
|
||||
// Open-settings action is removed in API-only settings mode.
|
||||
if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var showBackToDesktop = _isSettingsOpen;
|
||||
var buttonText = L("settings.back_to_desktop", "Back to Desktop");
|
||||
OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop;
|
||||
OpenSettingsButtonTextBlock.Text = buttonText;
|
||||
ToolTip.SetTip(
|
||||
OpenSettingsButton,
|
||||
showBackToDesktop
|
||||
? buttonText
|
||||
: L("tooltip.open_settings", "Settings"));
|
||||
|
||||
var effectiveCellSize = _currentDesktopCellSize > 0
|
||||
? _currentDesktopCellSize
|
||||
: Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells));
|
||||
ApplyWidgetSizing(effectiveCellSize);
|
||||
}
|
||||
|
||||
private void OpenComponentLibraryWindow()
|
||||
@@ -316,13 +378,109 @@ public partial class MainWindow
|
||||
_reopenSettingsAfterComponentLibraryClose = false;
|
||||
if (shouldReopenSettings)
|
||||
{
|
||||
AppLogger.Info(
|
||||
"SettingsFacade",
|
||||
"Reopen settings request ignored because settings UI entry is disabled during hard-cut migration.");
|
||||
(Application.Current as App)?.OpenIndependentSettingsModule("ComponentLibraryClose");
|
||||
}
|
||||
}, FluttermotionToken.Slow);
|
||||
}
|
||||
|
||||
private void OpenDetachedComponentLibraryWindow()
|
||||
{
|
||||
_detachedComponentLibraryWindow ??= CreateDetachedComponentLibraryWindow();
|
||||
_detachedComponentLibraryWindow.Reload();
|
||||
if (!_detachedComponentLibraryWindow.IsVisible)
|
||||
{
|
||||
if (IsVisible)
|
||||
{
|
||||
_detachedComponentLibraryWindow.Show(this);
|
||||
}
|
||||
else
|
||||
{
|
||||
_detachedComponentLibraryWindow.Show();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
_detachedComponentLibraryWindow.Activate();
|
||||
}
|
||||
|
||||
private void CloseDetachedComponentLibraryWindow()
|
||||
{
|
||||
if (_detachedComponentLibraryWindow is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_detachedComponentLibraryWindow.Hide();
|
||||
}
|
||||
|
||||
private ComponentLibraryWindow CreateDetachedComponentLibraryWindow()
|
||||
{
|
||||
var window = new ComponentLibraryWindow(
|
||||
_componentLibraryService,
|
||||
cellSize => new ComponentLibraryCreateContext(
|
||||
cellSize,
|
||||
_timeZoneService,
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_settingsFacade),
|
||||
L);
|
||||
window.AddComponentRequested += OnDetachedComponentLibraryAddComponentRequested;
|
||||
window.Closed += OnDetachedComponentLibraryClosed;
|
||||
return window;
|
||||
}
|
||||
|
||||
private void OnDetachedComponentLibraryAddComponentRequested(object? sender, string componentId)
|
||||
{
|
||||
_ = sender;
|
||||
if (string.IsNullOrWhiteSpace(componentId) ||
|
||||
_currentDesktopSurfaceIndex < 0 ||
|
||||
_currentDesktopSurfaceIndex >= _desktopPageCount ||
|
||||
!_desktopPageComponentGrids.TryGetValue(_currentDesktopSurfaceIndex, out var pageGrid) ||
|
||||
!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var descriptor))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var span = NormalizeComponentCellSpan(
|
||||
componentId,
|
||||
ComponentPlacementRules.EnsureMinimumSize(
|
||||
descriptor.Definition,
|
||||
descriptor.Definition.MinWidthCells,
|
||||
descriptor.Definition.MinHeightCells));
|
||||
var row = Math.Max(0, (pageGrid.RowDefinitions.Count - span.HeightCells) / 2);
|
||||
var column = Math.Max(0, (pageGrid.ColumnDefinitions.Count - span.WidthCells) / 2);
|
||||
PlaceDesktopComponentOnPage(componentId, _currentDesktopSurfaceIndex, row, column);
|
||||
}
|
||||
|
||||
private void OnDetachedComponentLibraryClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_ = e;
|
||||
if (ReferenceEquals(sender, _detachedComponentLibraryWindow))
|
||||
{
|
||||
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
|
||||
_detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed;
|
||||
_detachedComponentLibraryWindow = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnSettingsWindowStateChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
SyncSettingsWindowState();
|
||||
}
|
||||
|
||||
private void SyncSettingsWindowState()
|
||||
{
|
||||
var isOpen = (Application.Current as App)?.SettingsWindowService?.IsOpen == true;
|
||||
_isSettingsOpen = isOpen;
|
||||
UpdateDesktopPageAwareComponentContext();
|
||||
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
|
||||
UpdateOpenSettingsActionVisualState();
|
||||
}
|
||||
|
||||
private void InitializeDesktopComponentDragHandlers()
|
||||
{
|
||||
// Global handlers: we capture the pointer during drag, then track move/release anywhere.
|
||||
@@ -1245,6 +1403,21 @@ public partial class MainWindow
|
||||
return CreateDesktopComponentControl(runtimeDescriptor, _currentDesktopCellSize, placementId, pageIndex, "DesktopSurface");
|
||||
}
|
||||
|
||||
private Control? CreateDesktopComponentControl(
|
||||
string componentId,
|
||||
double cellSize,
|
||||
string? placementId,
|
||||
int? pageIndex,
|
||||
string action)
|
||||
{
|
||||
if (!_componentRuntimeRegistry.TryGetDescriptor(componentId, out var runtimeDescriptor))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return CreateDesktopComponentControl(runtimeDescriptor, cellSize, placementId, pageIndex, action);
|
||||
}
|
||||
|
||||
private Control? CreateDesktopComponentControl(
|
||||
DesktopComponentRuntimeDescriptor runtimeDescriptor,
|
||||
double cellSize,
|
||||
@@ -1260,6 +1433,7 @@ public partial class MainWindow
|
||||
_weatherDataService,
|
||||
_recommendationInfoService,
|
||||
_calculatorDataService,
|
||||
_settingsFacade,
|
||||
placementId);
|
||||
if (!_componentLibraryService.TryCreateControl(runtimeDescriptor.Definition.Id, createContext, out var component, out var exception) ||
|
||||
component is null)
|
||||
@@ -1291,6 +1465,7 @@ public partial class MainWindow
|
||||
}
|
||||
|
||||
internal bool IsComponentLibraryOpenFromService => _isComponentLibraryOpen;
|
||||
internal bool IsDetachedComponentLibraryWindowOpenFromService => _detachedComponentLibraryWindow is { IsVisible: true };
|
||||
|
||||
internal void OpenComponentLibraryWindowFromService()
|
||||
{
|
||||
@@ -1302,6 +1477,44 @@ public partial class MainWindow
|
||||
CloseComponentLibraryWindow(reopenSettings: false);
|
||||
}
|
||||
|
||||
internal void OpenDetachedComponentLibraryWindowFromService()
|
||||
{
|
||||
OpenDetachedComponentLibraryWindow();
|
||||
}
|
||||
|
||||
internal void CloseDetachedComponentLibraryWindowFromService()
|
||||
{
|
||||
CloseDetachedComponentLibraryWindow();
|
||||
}
|
||||
|
||||
public bool TryGetSettingsWindowAnchorBounds(out PixelRect anchorBounds)
|
||||
{
|
||||
anchorBounds = default;
|
||||
if (!IsVisible || BottomTaskbarContainer is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var origin = BottomTaskbarContainer.TranslatePoint(new Point(0, 0), this);
|
||||
if (origin is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var scale = RenderScaling > 0 ? RenderScaling : 1d;
|
||||
var width = (int)Math.Round(BottomTaskbarContainer.Bounds.Width * scale);
|
||||
var height = (int)Math.Round(BottomTaskbarContainer.Bounds.Height * scale);
|
||||
if (width <= 0 || height <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var x = Position.X + (int)Math.Round(origin.Value.X * scale);
|
||||
var y = Position.Y + (int)Math.Round(origin.Value.Y * scale);
|
||||
anchorBounds = new PixelRect(x, y, width, height);
|
||||
return true;
|
||||
}
|
||||
|
||||
private void CollapseComponentLibraryPanel()
|
||||
{
|
||||
// Animate component library panel collapsing downward
|
||||
@@ -1314,6 +1527,7 @@ public partial class MainWindow
|
||||
_isComponentLibraryOpen = false;
|
||||
CancelDesktopComponentDrag();
|
||||
CancelDesktopComponentResize(restoreOriginalSpan: true);
|
||||
CloseDetachedComponentLibraryWindow();
|
||||
ClearDesktopComponentSelection();
|
||||
ClearSelectedLauncherTile(refreshTaskbar: false);
|
||||
UpdateDesktopComponentHostEditState();
|
||||
@@ -2170,27 +2384,18 @@ public partial class MainWindow
|
||||
|
||||
private IReadOnlyList<ComponentLibraryCategory> GetComponentLibraryCategories()
|
||||
{
|
||||
var descriptors = _componentRuntimeRegistry.GetDesktopComponents();
|
||||
if (descriptors.Count == 0)
|
||||
var categories = _componentLibraryService.GetDesktopCategories();
|
||||
if (categories.Count == 0)
|
||||
{
|
||||
return Array.Empty<ComponentLibraryCategory>();
|
||||
}
|
||||
|
||||
return descriptors
|
||||
.GroupBy(descriptor => descriptor.Definition.Category, StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)
|
||||
.Select(group =>
|
||||
{
|
||||
var categoryId = string.IsNullOrWhiteSpace(group.Key) ? "Other" : group.Key.Trim();
|
||||
var components = group
|
||||
.OrderBy(descriptor => descriptor.Definition.DisplayName, StringComparer.OrdinalIgnoreCase)
|
||||
.ToList();
|
||||
return new ComponentLibraryCategory(
|
||||
categoryId,
|
||||
ResolveComponentLibraryCategoryIcon(categoryId),
|
||||
GetLocalizedComponentLibraryCategoryTitle(categoryId),
|
||||
components);
|
||||
})
|
||||
return categories
|
||||
.Select(category => new ComponentLibraryCategory(
|
||||
category.Id,
|
||||
ResolveComponentLibraryCategoryIcon(category.Id),
|
||||
GetLocalizedComponentLibraryCategoryTitle(category.Id),
|
||||
category.Components))
|
||||
.ToList();
|
||||
}
|
||||
|
||||
@@ -2411,12 +2616,7 @@ public partial class MainWindow
|
||||
|
||||
for (var i = 0; i < componentCount; i++)
|
||||
{
|
||||
var descriptor = _componentLibraryActiveComponents[i];
|
||||
var definition = descriptor.Definition;
|
||||
if (!definition.AllowDesktopPlacement)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
var component = _componentLibraryActiveComponents[i];
|
||||
|
||||
var page = new Grid
|
||||
{
|
||||
@@ -2429,8 +2629,8 @@ public partial class MainWindow
|
||||
var previewMaxWidth = _componentLibraryComponentPageWidth * 0.94;
|
||||
var previewMaxHeight = viewportHeight * 0.86;
|
||||
var previewSpan = NormalizeComponentCellSpan(
|
||||
definition.Id,
|
||||
(definition.MinWidthCells, definition.MinHeightCells));
|
||||
component.ComponentId,
|
||||
(component.MinWidthCells, component.MinHeightCells));
|
||||
var previewCellSize = Math.Min(
|
||||
previewMaxWidth / Math.Max(1, previewSpan.WidthCells),
|
||||
previewMaxHeight / Math.Max(1, previewSpan.HeightCells));
|
||||
@@ -2441,7 +2641,7 @@ public partial class MainWindow
|
||||
var renderCellSize = Math.Clamp(previewCellSize * 1.15, 26, 110);
|
||||
|
||||
var previewControl = CreateDesktopComponentControl(
|
||||
descriptor,
|
||||
component.ComponentId,
|
||||
renderCellSize,
|
||||
placementId: null,
|
||||
pageIndex: null,
|
||||
@@ -2479,13 +2679,13 @@ public partial class MainWindow
|
||||
Background = Brushes.Transparent,
|
||||
BorderThickness = new Thickness(0),
|
||||
Child = previewViewbox,
|
||||
Tag = definition.Id
|
||||
Tag = component.ComponentId
|
||||
};
|
||||
previewBorder.PointerPressed += OnComponentLibraryComponentPreviewPointerPressed;
|
||||
|
||||
var label = new TextBlock
|
||||
{
|
||||
Text = GetLocalizedComponentDisplayName(descriptor),
|
||||
Text = GetLocalizedComponentDisplayName(component),
|
||||
FontSize = 14,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush"),
|
||||
@@ -2544,11 +2744,11 @@ public partial class MainWindow
|
||||
ComponentLibraryComponentPagesContainer.ColumnDefinitions.Clear();
|
||||
}
|
||||
|
||||
private string GetLocalizedComponentDisplayName(DesktopComponentRuntimeDescriptor descriptor)
|
||||
private string GetLocalizedComponentDisplayName(ComponentLibraryComponentEntry component)
|
||||
{
|
||||
return string.IsNullOrWhiteSpace(descriptor.DisplayNameLocalizationKey)
|
||||
? descriptor.Definition.DisplayName
|
||||
: L(descriptor.DisplayNameLocalizationKey, descriptor.Definition.DisplayName);
|
||||
return string.IsNullOrWhiteSpace(component.DisplayNameLocalizationKey)
|
||||
? component.DisplayName
|
||||
: L(component.DisplayNameLocalizationKey, component.DisplayName);
|
||||
}
|
||||
|
||||
private void OnComponentLibraryComponentPreviewPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
|
||||
@@ -2,10 +2,12 @@ using System;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.Models;
|
||||
@@ -276,6 +278,13 @@ public partial class MainWindow
|
||||
private void ApplyNightModeState(bool enabled, bool refreshPalettes)
|
||||
{
|
||||
_isNightMode = enabled;
|
||||
var requestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light;
|
||||
RequestedThemeVariant = requestedThemeVariant;
|
||||
if (Application.Current is not null)
|
||||
{
|
||||
Application.Current.RequestedThemeVariant = requestedThemeVariant;
|
||||
}
|
||||
|
||||
if (!refreshPalettes)
|
||||
{
|
||||
return;
|
||||
@@ -335,13 +344,44 @@ public partial class MainWindow
|
||||
_suppressSettingsPersistence = true;
|
||||
try
|
||||
{
|
||||
InitializeLocalization(snapshot.LanguageCode);
|
||||
if (string.IsNullOrWhiteSpace(snapshot.TimeZoneId))
|
||||
{
|
||||
_timeZoneService.CurrentTimeZone = TimeZoneInfo.Local;
|
||||
}
|
||||
else
|
||||
{
|
||||
_timeZoneService.SetTimeZoneById(snapshot.TimeZoneId);
|
||||
}
|
||||
|
||||
_targetShortSideCells = Math.Clamp(
|
||||
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
|
||||
MinShortSideCells,
|
||||
MaxShortSideCells);
|
||||
_gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset(snapshot.GridSpacingPreset);
|
||||
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
|
||||
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
|
||||
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
|
||||
ApplyTaskbarSettings(snapshot);
|
||||
InitializeWeatherSettings(snapshot);
|
||||
InitializeAutoStartWithWindowsSetting(snapshot);
|
||||
InitializeAppRenderModeSetting(snapshot);
|
||||
InitializeUpdateSettings(snapshot);
|
||||
InitializeDesktopSurfaceState(layoutSnapshot);
|
||||
InitializeLauncherVisibilitySettings(launcherSnapshot);
|
||||
InitializeDesktopComponentPlacements(layoutSnapshot);
|
||||
TryRestoreWallpaper(snapshot.WallpaperPath);
|
||||
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
|
||||
{
|
||||
_selectedThemeColor = savedThemeColor;
|
||||
}
|
||||
|
||||
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
|
||||
ApplyNightModeState(_isNightMode, refreshPalettes: true);
|
||||
ApplyWallpaperBrush();
|
||||
UpdateWallpaperDisplay();
|
||||
InitializeTimeZoneSettings();
|
||||
ApplyLocalization();
|
||||
RebuildDesktopGrid();
|
||||
}
|
||||
finally
|
||||
|
||||
@@ -285,9 +285,9 @@
|
||||
Grid.Column="2"
|
||||
Background="Transparent"
|
||||
BorderThickness="0">
|
||||
<Grid ColumnDefinitions="Auto,Auto"
|
||||
<Grid ColumnDefinitions="Auto,Auto,Auto"
|
||||
ColumnSpacing="8">
|
||||
<Button x:Name="OpenComponentLibraryButton"
|
||||
<Button x:Name="OpenSettingsButton"
|
||||
Grid.Column="0"
|
||||
IsVisible="False"
|
||||
Padding="8"
|
||||
@@ -296,6 +296,31 @@
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnOpenSettingsClick"
|
||||
ToolTip.Tip="Settings">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="8">
|
||||
<fi:FluentIcon x:Name="OpenSettingsIcon"
|
||||
Icon="Settings"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock x:Name="OpenSettingsButtonTextBlock"
|
||||
IsVisible="False"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Text="Settings" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="OpenComponentLibraryButton"
|
||||
Grid.Column="1"
|
||||
IsVisible="False"
|
||||
Padding="8"
|
||||
HorizontalAlignment="Stretch"
|
||||
VerticalAlignment="Stretch"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
|
||||
Click="OnOpenComponentLibraryClick"
|
||||
ToolTip.Tip="组件库">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
|
||||
@@ -17,6 +17,7 @@ using Avalonia.Platform;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using Avalonia.VisualTree;
|
||||
using FluentAvalonia.Styling;
|
||||
using LanMountainDesktop.ComponentSystem;
|
||||
using LanMountainDesktop.Models;
|
||||
@@ -29,7 +30,7 @@ using LibVLCSharp.Shared;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
public partial class MainWindow : Window, ISettingsWindowAnchorProvider
|
||||
{
|
||||
private enum WallpaperPlacement
|
||||
{
|
||||
@@ -84,7 +85,7 @@ public partial class MainWindow : Window
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly IComponentLayoutStore _componentLayoutStore = ComponentDomainStorageProvider.Instance;
|
||||
private readonly IComponentStateStore _componentStateStore = ComponentDomainStorageProvider.Instance;
|
||||
private readonly IComponentInstanceSettingsStore _componentSettingsStore = new ComponentSettingsService();
|
||||
private readonly IComponentInstanceSettingsStore _componentSettingsStore = HostComponentSettingsStoreProvider.GetOrCreate();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
private readonly TimeZoneService _timeZoneService;
|
||||
private readonly WindowsStartupService _windowsStartupService = new();
|
||||
@@ -94,7 +95,8 @@ public partial class MainWindow : Window
|
||||
private readonly ComponentRegistry _componentRegistry;
|
||||
private readonly DesktopComponentRuntimeRegistry _componentRuntimeRegistry;
|
||||
private readonly IComponentLibraryService _componentLibraryService;
|
||||
private readonly IComponentLibraryWindowService _componentLibraryWindowService = new ComponentLibraryWindowService();
|
||||
private readonly IEmbeddedComponentLibraryService _componentLibraryWindowService = new EmbeddedComponentLibraryService();
|
||||
private ComponentLibraryWindow? _detachedComponentLibraryWindow;
|
||||
private readonly FluentAvaloniaTheme? _fluentAvaloniaTheme;
|
||||
private readonly HashSet<string> _topStatusComponentIds = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly HashSet<TaskbarActionId> _pinnedTaskbarActions = [];
|
||||
@@ -190,13 +192,19 @@ public partial class MainWindow : Window
|
||||
InitializeComponent();
|
||||
_componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry(
|
||||
_componentRegistry,
|
||||
pluginRuntimeService);
|
||||
pluginRuntimeService,
|
||||
_settingsFacade);
|
||||
_componentLibraryService = new ComponentLibraryService(_componentRegistry, _componentRuntimeRegistry);
|
||||
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
|
||||
_settingsService.Changed += OnSettingsChanged;
|
||||
PropertyChanged += OnWindowPropertyChanged;
|
||||
InitializeDesktopSurfaceSwipeHandlers();
|
||||
InitializeDesktopComponentDragHandlers();
|
||||
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
|
||||
{
|
||||
settingsWindowService.StateChanged += OnSettingsWindowStateChanged;
|
||||
_isSettingsOpen = settingsWindowService.IsOpen;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
|
||||
@@ -234,6 +242,7 @@ public partial class MainWindow : Window
|
||||
protected override void OnOpened(EventArgs e)
|
||||
{
|
||||
base.OnOpened(e);
|
||||
SyncSettingsWindowState();
|
||||
|
||||
_suppressSettingsPersistence = true;
|
||||
var snapshot = _settingsService.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App);
|
||||
@@ -295,6 +304,13 @@ public partial class MainWindow : Window
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
PersistSettings();
|
||||
if (_detachedComponentLibraryWindow is not null)
|
||||
{
|
||||
_detachedComponentLibraryWindow.AddComponentRequested -= OnDetachedComponentLibraryAddComponentRequested;
|
||||
_detachedComponentLibraryWindow.Closed -= OnDetachedComponentLibraryClosed;
|
||||
_detachedComponentLibraryWindow.Close();
|
||||
}
|
||||
_detachedComponentLibraryWindow = null;
|
||||
StopVideoWallpaper();
|
||||
DisposeLauncherResources();
|
||||
_videoWallpaperMedia?.Dispose();
|
||||
@@ -316,6 +332,10 @@ public partial class MainWindow : Window
|
||||
_settingsService.Changed -= OnSettingsChanged;
|
||||
PropertyChanged -= OnWindowPropertyChanged;
|
||||
DesktopHost.SizeChanged -= OnDesktopHostSizeChanged;
|
||||
if (Application.Current is App app && app.SettingsWindowService is { } settingsWindowService)
|
||||
{
|
||||
settingsWindowService.StateChanged -= OnSettingsWindowStateChanged;
|
||||
}
|
||||
base.OnClosed(e);
|
||||
}
|
||||
|
||||
@@ -985,6 +1005,17 @@ public partial class MainWindow : Window
|
||||
BackToWindowsIcon.FontSize = taskbarIconSize;
|
||||
BackToWindowsTextBlock.FontSize = taskbarTextSize;
|
||||
SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing);
|
||||
|
||||
OpenSettingsButton.Margin = new Thickness(0);
|
||||
OpenSettingsButton.Padding = taskbarButtonPadding;
|
||||
OpenSettingsButton.FontSize = taskbarTextSize;
|
||||
OpenSettingsButton.MinHeight = taskbarCellHeight;
|
||||
OpenSettingsButton.MinWidth = OpenSettingsButtonTextBlock.IsVisible
|
||||
? Math.Clamp(taskbarCellHeight * 2.35, 100, 340)
|
||||
: Math.Clamp(taskbarCellHeight * 1.10, 48, 88);
|
||||
OpenSettingsIcon.FontSize = taskbarIconSize;
|
||||
OpenSettingsButtonTextBlock.FontSize = taskbarTextSize;
|
||||
SetButtonContentSpacing(OpenSettingsButton, buttonContentSpacing);
|
||||
|
||||
OpenComponentLibraryButton.Margin = new Thickness(0);
|
||||
OpenComponentLibraryButton.Padding = taskbarButtonPadding;
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
|
||||
x:DataType="vm:AboutSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
|
||||
<controls:SettingsSectionCard IconKey="Info"
|
||||
Title="{Binding AppInfoHeader}">
|
||||
<controls:SettingsSectionCard.CardContent>
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding VersionLabel}" />
|
||||
<TextBlock Opacity="0.82"
|
||||
Text="{Binding VersionText}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding RenderBackendLabel}" />
|
||||
<TextBlock Opacity="0.82"
|
||||
Text="{Binding RenderBackendText}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</controls:SettingsSectionCard.CardContent>
|
||||
</controls:SettingsSectionCard>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding UpdateHeader}"
|
||||
IsExpanded="True">
|
||||
<StackPanel Spacing="12">
|
||||
<controls:SettingsOptionCard IconKey="ArrowSync"
|
||||
Title="{Binding AutoCheckUpdatesLabel}">
|
||||
<controls:SettingsOptionCard.ActionContent>
|
||||
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
|
||||
</controls:SettingsOptionCard.ActionContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="ArrowSync"
|
||||
Title="{Binding IncludePrereleaseUpdatesLabel}">
|
||||
<controls:SettingsOptionCard.ActionContent>
|
||||
<ToggleSwitch IsChecked="{Binding IncludePrereleaseUpdates}" />
|
||||
</controls:SettingsOptionCard.ActionContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="ArrowSync"
|
||||
Title="{Binding UpdateChannelLabel}">
|
||||
<controls:SettingsOptionCard.DetailsContent>
|
||||
<ComboBox ItemsSource="{Binding UpdateChannels}"
|
||||
SelectedItem="{Binding SelectedUpdateChannel}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsOptionCard.DetailsContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12">
|
||||
<Button Command="{Binding CheckForUpdatesCommand}"
|
||||
Content="{Binding CheckForUpdatesButtonText}" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Opacity="0.76"
|
||||
Text="{Binding UpdateStatus}" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,30 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"about",
|
||||
"About",
|
||||
SettingsPageCategory.About,
|
||||
IconKey = "Info",
|
||||
SortOrder = 40,
|
||||
TitleLocalizationKey = "settings.about.title",
|
||||
DescriptionLocalizationKey = "settings.about.description")]
|
||||
public partial class AboutSettingsPage : SettingsPageBase
|
||||
{
|
||||
public AboutSettingsPage()
|
||||
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public AboutSettingsPage(AboutSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AboutSettingsPageViewModel ViewModel { get; }
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage"
|
||||
x:DataType="vm:AppearanceSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
|
||||
<controls:SettingsOptionCard IconKey="DesignIdeas"
|
||||
Title="{Binding NightModeLabel}">
|
||||
<controls:SettingsOptionCard.ActionContent>
|
||||
<ToggleSwitch IsChecked="{Binding IsNightMode}" />
|
||||
</controls:SettingsOptionCard.ActionContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="DesignIdeas"
|
||||
Title="{Binding UseSystemChromeLabel}">
|
||||
<controls:SettingsOptionCard.ActionContent>
|
||||
<ToggleSwitch IsChecked="{Binding UseSystemChrome}" />
|
||||
</controls:SettingsOptionCard.ActionContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="DesignIdeas"
|
||||
Title="{Binding ThemeColorLabel}">
|
||||
<controls:SettingsOptionCard.DetailsContent>
|
||||
<TextBox Watermark="#AABBCC"
|
||||
Text="{Binding ThemeColor}" />
|
||||
</controls:SettingsOptionCard.DetailsContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsSectionCard IconKey="DesignIdeas"
|
||||
Title="{Binding WallpaperHeader}">
|
||||
<controls:SettingsSectionCard.CardContent>
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding WallpaperPathLabel}" />
|
||||
<TextBox IsReadOnly="True"
|
||||
Text="{Binding WallpaperPath}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding WallpaperPlacementLabel}" />
|
||||
<ComboBox ItemsSource="{Binding WallpaperPlacements}"
|
||||
SelectedItem="{Binding SelectedWallpaperPlacement}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
|
||||
<Button HorizontalAlignment="Left"
|
||||
Click="OnBrowseWallpaperClick"
|
||||
Content="{Binding ImportWallpaperButtonText}" />
|
||||
</StackPanel>
|
||||
</controls:SettingsSectionCard.CardContent>
|
||||
</controls:SettingsSectionCard>
|
||||
|
||||
<controls:SettingsSectionCard IconKey="DesignIdeas"
|
||||
Title="{Binding ClockHeader}"
|
||||
Description="{Binding ClockDescription}">
|
||||
<controls:SettingsSectionCard.CardContent>
|
||||
<StackPanel Spacing="14">
|
||||
<controls:SettingsOptionCard IconKey="DesignIdeas"
|
||||
Title="{Binding ClockHeader}">
|
||||
<controls:SettingsOptionCard.ActionContent>
|
||||
<ToggleSwitch IsChecked="{Binding ShowClock}" />
|
||||
</controls:SettingsOptionCard.ActionContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="DesignIdeas"
|
||||
Title="{Binding ClockFormatLabel}">
|
||||
<controls:SettingsOptionCard.DetailsContent>
|
||||
<ComboBox ItemsSource="{Binding ClockFormats}"
|
||||
SelectedItem="{Binding SelectedClockFormat}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsOptionCard.DetailsContent>
|
||||
</controls:SettingsOptionCard>
|
||||
</StackPanel>
|
||||
</controls:SettingsSectionCard.CardContent>
|
||||
</controls:SettingsSectionCard>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,54 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Platform.Storage;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using System.Linq;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"appearance",
|
||||
"Appearance",
|
||||
SettingsPageCategory.Appearance,
|
||||
IconKey = "DesignIdeas",
|
||||
SortOrder = 10,
|
||||
TitleLocalizationKey = "settings.appearance.title",
|
||||
DescriptionLocalizationKey = "settings.appearance.description")]
|
||||
public partial class AppearanceSettingsPage : SettingsPageBase
|
||||
{
|
||||
public AppearanceSettingsPage()
|
||||
: this(new AppearanceSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public AppearanceSettingsPage(AppearanceSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public AppearanceSettingsPageViewModel ViewModel { get; }
|
||||
|
||||
private async void OnBrowseWallpaperClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
if (TopLevel.GetTopLevel(this)?.StorageProvider is not { } storageProvider)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var files = await storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = ViewModel.FilePickerTitle,
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
var file = files.FirstOrDefault();
|
||||
var localPath = file?.TryGetLocalPath();
|
||||
if (!string.IsNullOrWhiteSpace(localPath))
|
||||
{
|
||||
await ViewModel.ImportWallpaperAsync(localPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage"
|
||||
x:DataType="vm:ComponentsSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding GridHeader}"
|
||||
IsExpanded="True">
|
||||
<StackPanel Spacing="14">
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding ShortSideCellsLabel}" />
|
||||
<NumericUpDown Minimum="6"
|
||||
Maximum="96"
|
||||
Value="{Binding ShortSideCells}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding EdgeInsetPercentLabel}" />
|
||||
<NumericUpDown Minimum="0"
|
||||
Maximum="30"
|
||||
Value="{Binding EdgeInsetPercent}" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Classes="settings-item">
|
||||
<TextBlock Classes="settings-item-label"
|
||||
Text="{Binding SpacingPresetLabel}" />
|
||||
<ComboBox ItemsSource="{Binding SpacingPresets}"
|
||||
SelectedItem="{Binding SelectedSpacingPreset}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,30 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"components",
|
||||
"Components",
|
||||
SettingsPageCategory.Components,
|
||||
IconKey = "GridDots",
|
||||
SortOrder = 20,
|
||||
TitleLocalizationKey = "settings.components.title",
|
||||
DescriptionLocalizationKey = "settings.components.description")]
|
||||
public partial class ComponentsSettingsPage : SettingsPageBase
|
||||
{
|
||||
public ComponentsSettingsPage()
|
||||
: this(new ComponentsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public ComponentsSettingsPage(ComponentsSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public ComponentsSettingsPageViewModel ViewModel { get; }
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:controls="using:LanMountainDesktop.Controls"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage"
|
||||
x:DataType="vm:GeneralSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
|
||||
<controls:SettingsOptionCard IconKey="Settings"
|
||||
Title="{Binding LanguageHeader}">
|
||||
<controls:SettingsOptionCard.DetailsContent>
|
||||
<ComboBox MinWidth="240"
|
||||
ItemsSource="{Binding Languages}"
|
||||
SelectedItem="{Binding SelectedLanguage}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsOptionCard.DetailsContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsOptionCard IconKey="Settings"
|
||||
Title="{Binding TimeZoneHeader}"
|
||||
Description="{Binding TimeZoneDescription}">
|
||||
<controls:SettingsOptionCard.DetailsContent>
|
||||
<ComboBox MinWidth="280"
|
||||
ItemsSource="{Binding TimeZones}"
|
||||
SelectedItem="{Binding SelectedTimeZone}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:TimeZoneOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</controls:SettingsOptionCard.DetailsContent>
|
||||
</controls:SettingsOptionCard>
|
||||
|
||||
<controls:SettingsSectionCard IconKey="Info"
|
||||
Title="{Binding PreviewHeader}">
|
||||
<controls:SettingsSectionCard.CardContent>
|
||||
<Grid ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="16"
|
||||
RowDefinitions="Auto,Auto"
|
||||
RowSpacing="12">
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
Text="{Binding PreviewTimeLabel}" />
|
||||
<TextBlock Grid.Column="1"
|
||||
Opacity="0.82"
|
||||
Text="{Binding PreviewTimeText}" />
|
||||
<TextBlock Grid.Row="1"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding PreviewDateLabel}" />
|
||||
<TextBlock Grid.Row="1"
|
||||
Grid.Column="1"
|
||||
Opacity="0.82"
|
||||
Text="{Binding PreviewDateText}" />
|
||||
</Grid>
|
||||
</controls:SettingsSectionCard.CardContent>
|
||||
</controls:SettingsSectionCard>
|
||||
|
||||
<ui:SettingsExpander Classes="settings-expander-card"
|
||||
Header="{Binding RuntimeHeader}"
|
||||
Description="{Binding RuntimeDescription}"
|
||||
IsExpanded="True">
|
||||
<ui:SettingsExpander.Footer>
|
||||
<ComboBox MinWidth="240"
|
||||
ItemsSource="{Binding RenderModes}"
|
||||
SelectedItem="{Binding SelectedRenderMode}">
|
||||
<ComboBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:SelectionOption">
|
||||
<TextBlock Text="{Binding Label}" />
|
||||
</DataTemplate>
|
||||
</ComboBox.ItemTemplate>
|
||||
</ComboBox>
|
||||
</ui:SettingsExpander.Footer>
|
||||
</ui:SettingsExpander>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,36 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"general",
|
||||
"General",
|
||||
SettingsPageCategory.General,
|
||||
IconKey = "Settings",
|
||||
SortOrder = 0,
|
||||
TitleLocalizationKey = "settings.general.title",
|
||||
DescriptionLocalizationKey = "settings.general.description")]
|
||||
public partial class GeneralSettingsPage : SettingsPageBase
|
||||
{
|
||||
public GeneralSettingsPage()
|
||||
: this(new GeneralSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public GeneralSettingsPage(GeneralSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.RestartRequested += OnRestartRequested;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public GeneralSettingsPageViewModel ViewModel { get; }
|
||||
|
||||
private void OnRestartRequested()
|
||||
{
|
||||
RequestRestart(ViewModel.RenderModeRestartMessage);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.GeneratedPluginSettingsPage"
|
||||
x:DataType="vm:PluginGeneratedSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding Title}" />
|
||||
<TextBlock x:Name="DescriptionTextBlock"
|
||||
Classes="settings-section-description"
|
||||
Text="{Binding Description}" />
|
||||
|
||||
<StackPanel x:Name="DynamicOptionsHost"
|
||||
Spacing="0" />
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,226 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.Controls;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
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")))
|
||||
{
|
||||
}
|
||||
|
||||
public GeneratedPluginSettingsPage(PluginGeneratedSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
|
||||
if (DescriptionTextBlock is not null)
|
||||
{
|
||||
DescriptionTextBlock.IsVisible = !string.IsNullOrWhiteSpace(ViewModel.Description);
|
||||
}
|
||||
|
||||
BuildDynamicOptions();
|
||||
}
|
||||
|
||||
public PluginGeneratedSettingsPageViewModel ViewModel { get; }
|
||||
private void BuildDynamicOptions()
|
||||
{
|
||||
if (DynamicOptionsHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DynamicOptionsHost.Children.Clear();
|
||||
foreach (var option in ViewModel.Section.Options)
|
||||
{
|
||||
DynamicOptionsHost.Children.Add(CreateOptionControl(option));
|
||||
}
|
||||
}
|
||||
|
||||
private Control CreateOptionControl(SettingsOptionDefinition option)
|
||||
{
|
||||
var title = ViewModel.Localizer.GetString(option.TitleLocalizationKey, option.TitleLocalizationKey);
|
||||
var description = string.IsNullOrWhiteSpace(option.DescriptionLocalizationKey)
|
||||
? null
|
||||
: ViewModel.Localizer.GetString(option.DescriptionLocalizationKey, option.DescriptionLocalizationKey);
|
||||
var card = new SettingsOptionCard
|
||||
{
|
||||
IconKey = "Settings",
|
||||
Title = title,
|
||||
Description = description
|
||||
};
|
||||
|
||||
switch (option.OptionType)
|
||||
{
|
||||
case SettingsOptionType.Toggle:
|
||||
card.ActionContent = CreateToggle(option);
|
||||
break;
|
||||
case SettingsOptionType.Number:
|
||||
card.DetailsContent = CreateNumber(option);
|
||||
break;
|
||||
case SettingsOptionType.Select:
|
||||
card.DetailsContent = CreateSelect(option);
|
||||
break;
|
||||
case SettingsOptionType.Path:
|
||||
card.DetailsContent = CreateText(option, "Path");
|
||||
break;
|
||||
case SettingsOptionType.List:
|
||||
card.DetailsContent = CreateText(option, "Comma-separated values");
|
||||
break;
|
||||
default:
|
||||
card.DetailsContent = CreateText(option, null);
|
||||
break;
|
||||
}
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
private Control CreateToggle(SettingsOptionDefinition option)
|
||||
{
|
||||
var toggleSwitch = new ToggleSwitch
|
||||
{
|
||||
IsChecked = ViewModel.SettingsService.GetValue<bool?>(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id) ?? (option.DefaultValue as bool? ?? false)
|
||||
};
|
||||
|
||||
toggleSwitch.IsCheckedChanged += (_, _) =>
|
||||
{
|
||||
ViewModel.SettingsService.SetValue(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
toggleSwitch.IsChecked == true,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id,
|
||||
changedKeys: [option.Key]);
|
||||
};
|
||||
|
||||
return toggleSwitch;
|
||||
}
|
||||
|
||||
private Control CreateNumber(SettingsOptionDefinition option)
|
||||
{
|
||||
var currentValue = ViewModel.SettingsService.GetValue<double?>(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id);
|
||||
|
||||
var numeric = new NumericUpDown
|
||||
{
|
||||
Minimum = (decimal)(option.Minimum ?? 0d),
|
||||
Maximum = (decimal)(option.Maximum ?? 9999d),
|
||||
Value = (decimal)(currentValue ?? Convert.ToDouble(option.DefaultValue ?? 0d))
|
||||
};
|
||||
|
||||
numeric.ValueChanged += (_, _) =>
|
||||
{
|
||||
ViewModel.SettingsService.SetValue(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
(double)(numeric.Value ?? 0m),
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id,
|
||||
changedKeys: [option.Key]);
|
||||
};
|
||||
|
||||
return numeric;
|
||||
}
|
||||
|
||||
private Control CreateSelect(SettingsOptionDefinition option)
|
||||
{
|
||||
var choices = option.Choices
|
||||
.Select(choice => new SelectionOption(
|
||||
choice.Value,
|
||||
ViewModel.Localizer.GetString(choice.TitleLocalizationKey, choice.TitleLocalizationKey)))
|
||||
.ToArray();
|
||||
|
||||
var comboBox = new ComboBox
|
||||
{
|
||||
ItemsSource = choices
|
||||
};
|
||||
|
||||
var currentValue = ViewModel.SettingsService.GetValue<string>(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id);
|
||||
comboBox.SelectedItem = choices.FirstOrDefault(choice =>
|
||||
string.Equals(choice.Value, currentValue ?? option.DefaultValue?.ToString(), StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
comboBox.SelectionChanged += (_, _) =>
|
||||
{
|
||||
if (comboBox.SelectedItem is not SelectionOption selected)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ViewModel.SettingsService.SetValue(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
selected.Value,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id,
|
||||
changedKeys: [option.Key]);
|
||||
};
|
||||
|
||||
return comboBox;
|
||||
}
|
||||
|
||||
private Control CreateText(SettingsOptionDefinition option, string? watermark)
|
||||
{
|
||||
var currentValue = option.OptionType == SettingsOptionType.List
|
||||
? string.Join(
|
||||
", ",
|
||||
ViewModel.SettingsService.GetValue<IReadOnlyList<string>>(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id) ?? (option.DefaultValue as IReadOnlyList<string> ?? []))
|
||||
: ViewModel.SettingsService.GetValue<string>(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id) ?? option.DefaultValue?.ToString() ?? string.Empty;
|
||||
|
||||
var textBox = new TextBox
|
||||
{
|
||||
Watermark = watermark,
|
||||
Text = currentValue
|
||||
};
|
||||
|
||||
textBox.LostFocus += (_, _) =>
|
||||
{
|
||||
object value = option.OptionType == SettingsOptionType.List
|
||||
? textBox.Text?
|
||||
.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.ToArray() ?? []
|
||||
: textBox.Text ?? string.Empty;
|
||||
|
||||
ViewModel.SettingsService.SetValue(
|
||||
SettingsScope.Plugin,
|
||||
option.Key,
|
||||
value,
|
||||
ViewModel.PluginId,
|
||||
sectionId: ViewModel.Section.Id,
|
||||
changedKeys: [option.Key]);
|
||||
};
|
||||
|
||||
return textBox;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.PluginsSettingsPage"
|
||||
x:Name="Root"
|
||||
x:DataType="vm:PluginsSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<TextBlock Classes="settings-section-title"
|
||||
Text="{Binding PageTitle}" />
|
||||
<TextBlock Classes="settings-section-description"
|
||||
Text="{Binding PageDescription}" />
|
||||
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="12"
|
||||
Margin="0,0,0,18">
|
||||
<Button Command="{Binding RefreshCommand}"
|
||||
Content="{Binding RefreshButtonText}" />
|
||||
<TextBlock VerticalAlignment="Center"
|
||||
Opacity="0.76"
|
||||
Text="{Binding StatusMessage}" />
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Classes="settings-subsection-title"
|
||||
Text="{Binding InstalledHeader}" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding InstalledPlugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:InstalledPluginItemViewModel">
|
||||
<Border Classes="settings-list-item">
|
||||
<Grid Classes="settings-list-actions"
|
||||
ColumnDefinitions="*,Auto,Auto">
|
||||
<StackPanel>
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Opacity="0.76"
|
||||
FontSize="12"
|
||||
Text="{Binding Description}" />
|
||||
<TextBlock Opacity="0.58"
|
||||
FontSize="11"
|
||||
Text="{Binding Version}" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1"
|
||||
IsChecked="{Binding IsEnabled}"
|
||||
Command="{Binding #Root.DataContext.TogglePluginCommand}"
|
||||
CommandParameter="{Binding}" />
|
||||
<Button Grid.Column="2"
|
||||
Command="{Binding #Root.DataContext.DeletePluginCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{Binding #Root.DataContext.DeleteButtonText}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
|
||||
<TextBlock Classes="settings-subsection-title"
|
||||
Text="{Binding MarketplaceHeader}" />
|
||||
|
||||
<ItemsControl ItemsSource="{Binding MarketPlugins}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:PluginMarketItemViewModel">
|
||||
<Border Classes="settings-list-item">
|
||||
<Grid Classes="settings-list-actions"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<StackPanel>
|
||||
<TextBlock FontWeight="SemiBold"
|
||||
Text="{Binding Name}" />
|
||||
<TextBlock Opacity="0.76"
|
||||
FontSize="12"
|
||||
Text="{Binding Description}" />
|
||||
<TextBlock Opacity="0.58"
|
||||
FontSize="11"
|
||||
Text="{Binding Version}" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1"
|
||||
Command="{Binding #Root.DataContext.InstallPluginCommand}"
|
||||
CommandParameter="{Binding}"
|
||||
Content="{Binding #Root.DataContext.InstallButtonText}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,41 @@
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
|
||||
namespace LanMountainDesktop.Views.SettingsPages;
|
||||
|
||||
[SettingsPageInfo(
|
||||
"plugins",
|
||||
"Plugins",
|
||||
SettingsPageCategory.Plugins,
|
||||
IconKey = "PuzzlePiece",
|
||||
SortOrder = 30,
|
||||
TitleLocalizationKey = "settings.plugins.title",
|
||||
DescriptionLocalizationKey = "settings.plugins.description")]
|
||||
public partial class PluginsSettingsPage : SettingsPageBase
|
||||
{
|
||||
public PluginsSettingsPage()
|
||||
: this(new PluginsSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
|
||||
{
|
||||
}
|
||||
|
||||
public PluginsSettingsPage(PluginsSettingsPageViewModel viewModel)
|
||||
{
|
||||
ViewModel = viewModel;
|
||||
ViewModel.RestartRequested += OnRestartRequested;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
public PluginsSettingsPageViewModel ViewModel { get; }
|
||||
|
||||
public override async void OnNavigatedTo(object? parameter)
|
||||
{
|
||||
await ViewModel.InitializeAsync();
|
||||
}
|
||||
|
||||
private void OnRestartRequested()
|
||||
{
|
||||
RequestRestart(ViewModel.RestartRequiredMessage);
|
||||
}
|
||||
}
|
||||
165
LanMountainDesktop/Views/SettingsWindow.axaml
Normal file
165
LanMountainDesktop/Views/SettingsWindow.axaml
Normal file
@@ -0,0 +1,165 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:LanMountainDesktop.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
x:Class="LanMountainDesktop.Views.SettingsWindow"
|
||||
x:DataType="vm:SettingsWindowViewModel"
|
||||
Width="1120"
|
||||
Height="760"
|
||||
MinWidth="920"
|
||||
MinHeight="620"
|
||||
CanResize="True"
|
||||
WindowStartupLocation="Manual"
|
||||
SystemDecorations="BorderOnly"
|
||||
FontFamily="{DynamicResource AppFontFamily}"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
Icon="/Assets/avalonia-logo.ico"
|
||||
Title="{Binding Title}">
|
||||
|
||||
<Grid Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
RowDefinitions="Auto,Auto,*">
|
||||
<Border x:Name="WindowTitleBarHost"
|
||||
Height="48"
|
||||
Padding="12,0,12,0"
|
||||
Background="{DynamicResource AdaptiveSurfaceBaseBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="0,0,0,1"
|
||||
PointerPressed="OnWindowTitleBarPointerPressed">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
|
||||
ColumnSpacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Button x:Name="TogglePaneButton"
|
||||
Width="40"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnTogglePaneButtonClick">
|
||||
<fi:FluentIcon x:Name="TogglePaneButtonIcon"
|
||||
Icon="PanelLeftExpand"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
|
||||
<fi:FluentIcon x:Name="WindowBrandIcon"
|
||||
Grid.Column="1"
|
||||
Margin="6,0,2,0"
|
||||
Icon="Settings"
|
||||
IconVariant="Filled"
|
||||
IsHitTestVisible="False"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<StackPanel Grid.Column="2"
|
||||
Orientation="Horizontal"
|
||||
VerticalAlignment="Center"
|
||||
Spacing="10">
|
||||
<TextBlock x:Name="WindowTitleTextBlock"
|
||||
FontSize="12"
|
||||
FontWeight="SemiBold"
|
||||
IsHitTestVisible="False"
|
||||
Text="{Binding Title}" />
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Name="RestartNowButton"
|
||||
Grid.Column="3"
|
||||
Padding="10,6"
|
||||
Margin="0,0,4,0"
|
||||
Background="Transparent"
|
||||
IsVisible="{Binding IsRestartRequested}"
|
||||
Click="OnRestartNowClick">
|
||||
<StackPanel Orientation="Horizontal"
|
||||
Spacing="6">
|
||||
<fi:FluentIcon x:Name="RestartButtonIcon"
|
||||
Icon="ArrowSync"
|
||||
IconVariant="Regular" />
|
||||
<TextBlock x:Name="RestartButtonTextBlock"
|
||||
Text="{Binding RestartButtonText}" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="CloseWindowButton"
|
||||
Grid.Column="4"
|
||||
Width="40"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseWindowClick">
|
||||
<fi:FluentIcon x:Name="CloseWindowButtonIcon"
|
||||
Icon="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<ui:InfoBar x:Name="RestartInfoBar"
|
||||
Grid.Row="1"
|
||||
IsOpen="{Binding IsRestartRequested}"
|
||||
Margin="16,8,16,0"
|
||||
Severity="Informational"
|
||||
IsClosable="False"
|
||||
Title="{Binding RestartTitle}"
|
||||
Message="{Binding RestartMessage}">
|
||||
<ui:InfoBar.ActionButton>
|
||||
<Button Click="OnRestartNowClick"
|
||||
Content="{Binding RestartButtonText}" />
|
||||
</ui:InfoBar.ActionButton>
|
||||
</ui:InfoBar>
|
||||
|
||||
<ui:NavigationView x:Name="RootNavigationView"
|
||||
Grid.Row="2"
|
||||
Margin="0,8,0,0"
|
||||
Background="Transparent"
|
||||
PaneDisplayMode="Auto"
|
||||
OpenPaneLength="283"
|
||||
IsSettingsVisible="False"
|
||||
IsPaneToggleButtonVisible="False"
|
||||
IsBackButtonVisible="False"
|
||||
SelectionChanged="OnNavigationSelectionChanged">
|
||||
<ui:NavigationView.Resources>
|
||||
<SolidColorBrush x:Key="NavigationViewContentBackground" Color="Transparent" />
|
||||
<SolidColorBrush x:Key="NavigationViewContentGridBorderBrush" Color="Transparent" />
|
||||
</ui:NavigationView.Resources>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="20"
|
||||
Margin="12,0,16,16">
|
||||
<ui:Frame x:Name="ContentFrame" />
|
||||
|
||||
<Border x:Name="DrawerBorder"
|
||||
Grid.Column="1"
|
||||
Width="296"
|
||||
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
|
||||
BorderBrush="{DynamicResource AdaptiveGlassPanelBorderBrush}"
|
||||
BorderThickness="1"
|
||||
CornerRadius="16"
|
||||
Padding="20"
|
||||
BoxShadow="0 8 24 #14000000"
|
||||
IsVisible="{Binding IsDrawerOpen}">
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="16">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="DrawerTitleTextBlock"
|
||||
FontSize="16"
|
||||
FontWeight="SemiBold"
|
||||
Text="{Binding DrawerTitle}" />
|
||||
<Button Grid.Column="1"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
Background="Transparent"
|
||||
BorderThickness="0"
|
||||
Click="OnCloseDrawerClick">
|
||||
<fi:FluentIcon Icon="Dismiss"
|
||||
IconVariant="Regular" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<ContentControl x:Name="DrawerContentHost"
|
||||
Grid.Row="1" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</ui:NavigationView>
|
||||
</Grid>
|
||||
</Window>
|
||||
467
LanMountainDesktop/Views/SettingsWindow.axaml.cs
Normal file
467
LanMountainDesktop/Views/SettingsWindow.axaml.cs
Normal file
@@ -0,0 +1,467 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Platform;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.ViewModels;
|
||||
using Symbol = FluentIcons.Common.Symbol;
|
||||
|
||||
namespace LanMountainDesktop.Views;
|
||||
|
||||
public partial class SettingsWindow : Window, ISettingsPageHostContext
|
||||
{
|
||||
private readonly ISettingsPageRegistry _pageRegistry;
|
||||
private readonly IHostApplicationLifecycle _hostApplicationLifecycle;
|
||||
private readonly Dictionary<string, Control> _cachedPages = new(StringComparer.OrdinalIgnoreCase);
|
||||
private readonly bool _useSystemChrome;
|
||||
|
||||
public SettingsWindow()
|
||||
: this(
|
||||
new SettingsWindowViewModel(),
|
||||
EmptySettingsPageRegistry.Instance,
|
||||
new HostApplicationLifecycleService())
|
||||
{
|
||||
}
|
||||
|
||||
public SettingsWindow(
|
||||
SettingsWindowViewModel viewModel,
|
||||
ISettingsPageRegistry pageRegistry,
|
||||
IHostApplicationLifecycle hostApplicationLifecycle,
|
||||
bool useSystemChrome = false)
|
||||
{
|
||||
_useSystemChrome = useSystemChrome;
|
||||
ViewModel = viewModel;
|
||||
_pageRegistry = pageRegistry;
|
||||
_hostApplicationLifecycle = hostApplicationLifecycle;
|
||||
DataContext = ViewModel;
|
||||
InitializeComponent();
|
||||
ApplyChromeMode(useSystemChrome);
|
||||
|
||||
Opened += OnOpened;
|
||||
SizeChanged += OnWindowSizeChanged;
|
||||
Closed += OnClosed;
|
||||
Loaded += OnLoaded;
|
||||
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
|
||||
}
|
||||
|
||||
public SettingsWindowViewModel ViewModel { get; }
|
||||
|
||||
private void OnLoaded(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
SyncPendingRestartState();
|
||||
SyncTitleText();
|
||||
UpdateChromeMetrics();
|
||||
UpdatePaneToggleIcon();
|
||||
}
|
||||
|
||||
public void ReloadPages(string? pageId)
|
||||
{
|
||||
ViewModel.Pages.Clear();
|
||||
foreach (var page in _pageRegistry.GetPages().Where(page => !page.HideDefault))
|
||||
{
|
||||
ViewModel.Pages.Add(page);
|
||||
}
|
||||
|
||||
_cachedPages.Clear();
|
||||
CloseDrawer();
|
||||
RebuildNavigationItems();
|
||||
NavigateTo(pageId ?? ViewModel.Pages.FirstOrDefault()?.PageId);
|
||||
}
|
||||
|
||||
public void OpenDrawer(Control content, string? title = null)
|
||||
{
|
||||
if (DrawerContentHost is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
DrawerContentHost.Content = content;
|
||||
ViewModel.DrawerTitle = title ?? ViewModel.DrawerFallbackTitle;
|
||||
ViewModel.IsDrawerOpen = true;
|
||||
SyncTitleText();
|
||||
}
|
||||
|
||||
public void CloseDrawer()
|
||||
{
|
||||
if (DrawerContentHost is not null)
|
||||
{
|
||||
DrawerContentHost.Content = null;
|
||||
}
|
||||
|
||||
ViewModel.IsDrawerOpen = false;
|
||||
ViewModel.DrawerTitle = null;
|
||||
SyncTitleText();
|
||||
}
|
||||
|
||||
public void RequestRestart(string? reason = null)
|
||||
{
|
||||
ViewModel.RestartMessage = string.IsNullOrWhiteSpace(reason)
|
||||
? ViewModel.GetDefaultRestartMessage()
|
||||
: reason;
|
||||
ViewModel.IsRestartRequested = true;
|
||||
}
|
||||
|
||||
public void ApplyChromeMode(bool useSystemChrome)
|
||||
{
|
||||
if (useSystemChrome || OperatingSystem.IsMacOS())
|
||||
{
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = -1;
|
||||
SystemDecorations = SystemDecorations.Full;
|
||||
return;
|
||||
}
|
||||
|
||||
SystemDecorations = SystemDecorations.BorderOnly;
|
||||
ExtendClientAreaToDecorationsHint = true;
|
||||
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||
ExtendClientAreaTitleBarHeightHint = 48;
|
||||
}
|
||||
|
||||
public void RefreshShellText()
|
||||
{
|
||||
SyncPendingRestartState();
|
||||
SyncTitleText();
|
||||
}
|
||||
|
||||
private void RebuildNavigationItems()
|
||||
{
|
||||
if (RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RootNavigationView.MenuItems.Clear();
|
||||
SettingsPageCategory? previousCategory = null;
|
||||
|
||||
foreach (var page in ViewModel.Pages)
|
||||
{
|
||||
if (previousCategory is not null && previousCategory != page.Category)
|
||||
{
|
||||
RootNavigationView.MenuItems.Add(new NavigationViewItemSeparator());
|
||||
}
|
||||
|
||||
RootNavigationView.MenuItems.Add(new NavigationViewItem
|
||||
{
|
||||
Content = page.Title,
|
||||
Tag = page.PageId,
|
||||
IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
|
||||
{
|
||||
Symbol = MapIcon(page.IconKey),
|
||||
IconVariant = FluentIcons.Common.IconVariant.Regular
|
||||
}
|
||||
});
|
||||
|
||||
previousCategory = page.Category;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNavigationSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e)
|
||||
{
|
||||
var selectedItem = e.SelectedItemContainer ?? e.SelectedItem as NavigationViewItem;
|
||||
NavigateTo(selectedItem?.Tag as string);
|
||||
}
|
||||
|
||||
private void NavigateTo(string? pageId)
|
||||
{
|
||||
var descriptor = ResolveDescriptor(pageId);
|
||||
if (descriptor is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var page = GetOrCreatePage(descriptor);
|
||||
if (page is SettingsPageBase settingsPage)
|
||||
{
|
||||
settingsPage.InitializeHostContext(this);
|
||||
settingsPage.NavigationUri = new Uri($"lmd://settings/{descriptor.PageId}", UriKind.Absolute);
|
||||
settingsPage.OnNavigatedTo(null);
|
||||
}
|
||||
|
||||
if (ContentFrame is not null)
|
||||
{
|
||||
ContentFrame.Content = page;
|
||||
}
|
||||
|
||||
ViewModel.CurrentPageTitle = descriptor.Title;
|
||||
ViewModel.CurrentPageDescription = descriptor.Description;
|
||||
ViewModel.CurrentPageId = descriptor.PageId;
|
||||
TrySelectNavigationItem(descriptor.PageId);
|
||||
SyncTitleText();
|
||||
}
|
||||
|
||||
private SettingsPageDescriptor? ResolveDescriptor(string? pageId)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(pageId) &&
|
||||
_pageRegistry.TryGetPage(pageId, out var descriptor) &&
|
||||
descriptor is not null)
|
||||
{
|
||||
return descriptor;
|
||||
}
|
||||
|
||||
return ViewModel.Pages.FirstOrDefault();
|
||||
}
|
||||
|
||||
private Control GetOrCreatePage(SettingsPageDescriptor descriptor)
|
||||
{
|
||||
if (_cachedPages.TryGetValue(descriptor.PageId, out var page))
|
||||
{
|
||||
return page;
|
||||
}
|
||||
|
||||
page = descriptor.CreatePage(this);
|
||||
if (page is SettingsPageBase settingsPage)
|
||||
{
|
||||
settingsPage.InitializeHostContext(this);
|
||||
}
|
||||
|
||||
_cachedPages[descriptor.PageId] = page;
|
||||
return page;
|
||||
}
|
||||
|
||||
private void TrySelectNavigationItem(string pageId)
|
||||
{
|
||||
if (RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var item in RootNavigationView.MenuItems.OfType<NavigationViewItem>())
|
||||
{
|
||||
if (string.Equals(item.Tag as string, pageId, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
RootNavigationView.SelectedItem = item;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRestartNowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
_hostApplicationLifecycle.TryRestart(new HostApplicationLifecycleRequest(
|
||||
Source: "SettingsWindow",
|
||||
Reason: "User accepted restart from settings window."));
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void OnCloseDrawerClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
CloseDrawer();
|
||||
}
|
||||
|
||||
private void OnPendingRestartStateChanged()
|
||||
{
|
||||
SyncPendingRestartState();
|
||||
}
|
||||
|
||||
private void SyncPendingRestartState()
|
||||
{
|
||||
if (!PendingRestartStateService.HasPendingRestart && !ViewModel.IsRestartRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (PendingRestartStateService.HasPendingRestart && string.IsNullOrWhiteSpace(ViewModel.RestartMessage))
|
||||
{
|
||||
ViewModel.RestartMessage = ViewModel.GetDefaultRestartMessage();
|
||||
}
|
||||
|
||||
ViewModel.IsRestartRequested = ViewModel.IsRestartRequested || PendingRestartStateService.HasPendingRestart;
|
||||
}
|
||||
|
||||
private void OnOpened(object? sender, EventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
UpdateChromeMetrics();
|
||||
}
|
||||
|
||||
private void OnWindowSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
UpdateChromeMetrics();
|
||||
}
|
||||
|
||||
private void OnClosed(object? sender, EventArgs e)
|
||||
{
|
||||
_cachedPages.Clear();
|
||||
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
|
||||
Opened -= OnOpened;
|
||||
SizeChanged -= OnWindowSizeChanged;
|
||||
}
|
||||
|
||||
private void OnWindowTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnTogglePaneButtonClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
if (RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
RootNavigationView.IsPaneOpen = !RootNavigationView.IsPaneOpen;
|
||||
UpdatePaneToggleIcon();
|
||||
}
|
||||
|
||||
private void OnCloseWindowClick(object? sender, Avalonia.Interactivity.RoutedEventArgs e)
|
||||
{
|
||||
_ = sender;
|
||||
_ = e;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void UpdatePaneToggleIcon()
|
||||
{
|
||||
if (TogglePaneButtonIcon is null || RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TogglePaneButtonIcon.Icon = RootNavigationView.IsPaneOpen
|
||||
? FluentIcons.Common.Icon.PanelLeftContract
|
||||
: FluentIcons.Common.Icon.PanelLeftExpand;
|
||||
}
|
||||
|
||||
private void UpdateChromeMetrics()
|
||||
{
|
||||
if (_useSystemChrome)
|
||||
{
|
||||
if (WindowTitleBarHost is { })
|
||||
{
|
||||
WindowTitleBarHost.IsVisible = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (WindowTitleBarHost is null ||
|
||||
TogglePaneButton is null ||
|
||||
TogglePaneButtonIcon is null ||
|
||||
WindowBrandIcon is null ||
|
||||
WindowTitleTextBlock is null ||
|
||||
RestartNowButton is null ||
|
||||
RestartButtonIcon is null ||
|
||||
RestartButtonTextBlock is null ||
|
||||
CloseWindowButton is null ||
|
||||
CloseWindowButtonIcon is null ||
|
||||
DrawerTitleTextBlock is null ||
|
||||
RootNavigationView is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var width = Bounds.Width > 1 ? Bounds.Width : Math.Max(Width, MinWidth);
|
||||
var height = Bounds.Height > 1 ? Bounds.Height : Math.Max(Height, MinHeight);
|
||||
var layoutScale = Math.Clamp(Math.Min(width / 1120d, height / 760d), 0.90, 1.18);
|
||||
|
||||
var titleBarHeight = Math.Clamp(48d * layoutScale, 44d, 58d);
|
||||
var titleBarButtonWidth = Math.Clamp(40d * layoutScale, 36d, 48d);
|
||||
var titleBarButtonHeight = Math.Clamp(32d * layoutScale, 30d, 38d);
|
||||
var titleFontSize = Math.Clamp(12d * layoutScale, 11d, 14d);
|
||||
var titleBarIconSize = Math.Clamp(16d * layoutScale, 15d, 20d);
|
||||
var drawerTitleFontSize = Math.Clamp(16d * layoutScale, 14d, 20d);
|
||||
var chromePadding = Math.Clamp(12d * layoutScale, 10d, 18d);
|
||||
var restartSpacing = Math.Clamp(6d * layoutScale, 6d, 10d);
|
||||
|
||||
ExtendClientAreaTitleBarHeightHint = titleBarHeight;
|
||||
|
||||
WindowTitleBarHost.Height = titleBarHeight;
|
||||
WindowTitleBarHost.Padding = new Thickness(chromePadding, 0, chromePadding, 0);
|
||||
|
||||
TogglePaneButton.Width = titleBarButtonWidth;
|
||||
TogglePaneButton.Height = titleBarButtonHeight;
|
||||
TogglePaneButtonIcon.FontSize = titleBarIconSize;
|
||||
WindowBrandIcon.FontSize = titleBarIconSize + 2;
|
||||
|
||||
WindowTitleTextBlock.FontSize = titleFontSize;
|
||||
|
||||
RestartNowButton.Padding = new Thickness(chromePadding * 0.9, Math.Max(6, chromePadding * 0.5));
|
||||
if (RestartNowButton.Content is StackPanel restartStack)
|
||||
{
|
||||
restartStack.Spacing = restartSpacing;
|
||||
}
|
||||
|
||||
RestartButtonIcon.FontSize = titleBarIconSize;
|
||||
RestartButtonTextBlock.FontSize = titleFontSize;
|
||||
|
||||
CloseWindowButton.Width = titleBarButtonWidth;
|
||||
CloseWindowButton.Height = titleBarButtonHeight;
|
||||
CloseWindowButtonIcon.FontSize = titleBarIconSize;
|
||||
|
||||
DrawerTitleTextBlock.FontSize = drawerTitleFontSize;
|
||||
|
||||
RootNavigationView.OpenPaneLength = Math.Clamp(283d * layoutScale, 248d, 320d);
|
||||
}
|
||||
|
||||
private void SyncTitleText()
|
||||
{
|
||||
Title = ViewModel.Title;
|
||||
|
||||
if (_useSystemChrome)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (WindowTitleTextBlock is null ||
|
||||
DrawerTitleTextBlock is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
WindowTitleTextBlock.Text = ViewModel.Title;
|
||||
DrawerTitleTextBlock.IsVisible = !string.IsNullOrWhiteSpace(ViewModel.DrawerTitle);
|
||||
}
|
||||
|
||||
private sealed class EmptySettingsPageRegistry : ISettingsPageRegistry
|
||||
{
|
||||
public static EmptySettingsPageRegistry Instance { get; } = new();
|
||||
|
||||
public void Rebuild()
|
||||
{
|
||||
}
|
||||
|
||||
public IReadOnlyList<SettingsPageDescriptor> GetPages()
|
||||
{
|
||||
return Array.Empty<SettingsPageDescriptor>();
|
||||
}
|
||||
|
||||
public bool TryGetPage(string pageId, out SettingsPageDescriptor? descriptor)
|
||||
{
|
||||
descriptor = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static Symbol MapIcon(string iconKey)
|
||||
{
|
||||
return iconKey?.Trim() switch
|
||||
{
|
||||
"DesignIdeas" => Symbol.Color,
|
||||
"GridDots" => Symbol.GridDots,
|
||||
"PuzzlePiece" => Symbol.PuzzlePiece,
|
||||
"Info" => Symbol.Info,
|
||||
"ArrowSync" => Symbol.ArrowSync,
|
||||
_ => Symbol.Settings
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
using LanMountainDesktop.Services;
|
||||
using LanMountainDesktop.Services.Settings;
|
||||
using LanMountainDesktop.PluginSdk;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
@@ -310,10 +311,15 @@ public sealed class PluginLoader
|
||||
services.AddSingleton(runtimeContext.Manifest);
|
||||
services.AddSingleton<IReadOnlyDictionary<string, object?>>(runtimeContext.Properties);
|
||||
services.AddSingleton<IPluginMessageBus, PluginMessageBus>();
|
||||
services.AddSingleton<IPluginSettingsService>(provider =>
|
||||
new PluginScopedSettingsService(
|
||||
runtimeContext.Manifest.Id,
|
||||
provider.GetRequiredService<ISettingsService>()));
|
||||
|
||||
RegisterHostService<IPluginPackageManager>(services, hostServices);
|
||||
RegisterHostService<IHostApplicationLifecycle>(services, hostServices);
|
||||
RegisterHostService<IPluginExportRegistry>(services, hostServices);
|
||||
RegisterHostService<ISettingsFacadeService>(services, hostServices);
|
||||
RegisterHostService<ISettingsService>(services, hostServices);
|
||||
RegisterHostService<ISettingsCatalog>(services, hostServices);
|
||||
|
||||
|
||||
@@ -55,6 +55,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
_packageManager,
|
||||
_applicationLifecycle,
|
||||
_exportRegistry,
|
||||
_settingsFacade,
|
||||
_settingsFacade.Settings,
|
||||
_settingsFacade.Catalog);
|
||||
_loaderOptions = CreateOptions();
|
||||
@@ -824,6 +825,7 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
private readonly IPluginPackageManager _packageManager;
|
||||
private readonly IHostApplicationLifecycle _applicationLifecycle;
|
||||
private readonly IPluginExportRegistry _exportRegistry;
|
||||
private readonly ISettingsFacadeService _settingsFacade;
|
||||
private readonly ISettingsService _settingsService;
|
||||
private readonly ISettingsCatalog _settingsCatalog;
|
||||
|
||||
@@ -831,12 +833,14 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
IPluginPackageManager packageManager,
|
||||
IHostApplicationLifecycle applicationLifecycle,
|
||||
IPluginExportRegistry exportRegistry,
|
||||
ISettingsFacadeService settingsFacade,
|
||||
ISettingsService settingsService,
|
||||
ISettingsCatalog settingsCatalog)
|
||||
{
|
||||
_packageManager = packageManager;
|
||||
_applicationLifecycle = applicationLifecycle;
|
||||
_exportRegistry = exportRegistry;
|
||||
_settingsFacade = settingsFacade;
|
||||
_settingsService = settingsService;
|
||||
_settingsCatalog = settingsCatalog;
|
||||
}
|
||||
@@ -858,6 +862,11 @@ public sealed class PluginRuntimeService : IDisposable
|
||||
return _exportRegistry;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(ISettingsFacadeService))
|
||||
{
|
||||
return _settingsFacade;
|
||||
}
|
||||
|
||||
if (serviceType == typeof(ISettingsService))
|
||||
{
|
||||
return _settingsService;
|
||||
|
||||
Reference in New Issue
Block a user