setting_re1

This commit is contained in:
lincube
2026-03-12 21:01:23 +08:00
parent 4679ee006f
commit 40a3a00cfe
42 changed files with 2367 additions and 1017 deletions

View File

@@ -9,9 +9,9 @@
<OutputPath>bin\$(Configuration)\$(TargetFramework)\content\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<PluginPackageOutputDirectory>..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\</PluginPackageOutputDirectory>
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).laapp</PluginPackagePath>
<LegacyLoosePluginOutputDirectory>..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\SamplePlugin\</LegacyLoosePluginOutputDirectory>
<PluginPackageOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Packages\</PluginPackageOutputDirectory>
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp</PluginPackagePath>
<LegacyLoosePluginOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Loose\</LegacyLoosePluginOutputDirectory>
</PropertyGroup>
<ItemGroup>

View File

@@ -1,32 +1,50 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System;
using System.Linq;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Threading;
using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
using AvaloniaWebView;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop;
public partial class App : Application
{
private enum DesktopShellState
{
ForegroundDesktop = 0,
MinimizedToTaskbar = 1,
TrayOnly = 2
}
private enum ShutdownIntent
{
None = 0,
ExitRequested = 1,
RestartRequested = 2
}
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private bool _exitCleanupCompleted;
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
private SettingsWindow? _traySettingsWindow;
private readonly IndependentSettingsModuleService _independentSettingsModuleService = new();
private TrayIcons? _trayIcons;
private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
private bool _mainWindowClosed;
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -35,6 +53,11 @@ public partial class App : Application
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
_independentSettingsModuleService.ShowOrActivate(source, pageTag);
}
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
@@ -62,12 +85,8 @@ public partial class App : Application
AppLogger.Info("App", "Desktop lifetime exit triggered.");
PerformExitCleanup();
};
desktop.MainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
};
AppLogger.Info("App", $"Main window created. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
}
@@ -81,42 +100,14 @@ public partial class App : Application
Reason: "User selected Exit App from the tray menu."));
}
private void OnTrayShowDesktopClick(object? sender, EventArgs e)
{
RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu");
}
private void OnTraySettingsClick(object? sender, EventArgs e)
{
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime)
{
return;
}
Dispatcher.UIThread.Post(() =>
{
try
{
if (_traySettingsWindow is { } existingWindow && existingWindow.IsVisible)
{
existingWindow.WindowState = Avalonia.Controls.WindowState.Normal;
existingWindow.Activate();
return;
}
var settingsWindow = new SettingsWindow();
settingsWindow.Closed += (_, _) =>
{
if (ReferenceEquals(_traySettingsWindow, settingsWindow))
{
_traySettingsWindow = null;
}
};
_traySettingsWindow = settingsWindow;
settingsWindow.Show();
settingsWindow.Activate();
}
catch (Exception ex)
{
AppLogger.Warn("TraySettings", "Failed to open settings window.", ex);
}
}, DispatcherPriority.Normal);
OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayRestartClick(object? sender, EventArgs e)
@@ -209,19 +200,25 @@ public partial class App : Application
{
var menu = new NativeMenu();
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "设置"));
var showDesktopItem = new NativeMenuItem(L("tray.menu.show_desktop", "Open Desktop"));
showDesktopItem.Click += OnTrayShowDesktopClick;
menu.Items.Add(showDesktopItem);
menu.Items.Add(new NativeMenuItemSeparator());
var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
settingsItem.Click += OnTraySettingsClick;
menu.Items.Add(settingsItem);
menu.Items.Add(new NativeMenuItemSeparator());
var restartItem = new NativeMenuItem(L("tray.menu.restart", "重启应用"));
var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
restartItem.Click += OnTrayRestartClick;
menu.Items.Add(restartItem);
menu.Items.Add(new NativeMenuItemSeparator());
var exitItem = new NativeMenuItem(L("tray.menu.exit", "退出应用"));
var exitItem = new NativeMenuItem(L("tray.menu.exit", "Exit App"));
exitItem.Click += OnTrayExitClick;
menu.Items.Add(exitItem);
@@ -245,6 +242,11 @@ public partial class App : Application
}
private void ActivateMainWindow()
{
RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance");
}
private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source)
{
Dispatcher.UIThread.Post(() =>
{
@@ -253,13 +255,11 @@ public partial class App : Application
return;
}
if (desktop.MainWindow is not Window mainWindow)
{
return;
}
try
{
var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible)
{
mainWindow.Show();
@@ -278,18 +278,68 @@ public partial class App : Application
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
if (mainWindow is MainWindow lanMountainMainWindow)
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
AppLogger.Info(
"DesktopShell",
$"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; ShowSingleInstanceNotice={showSingleInstanceNotice}; WindowState='{mainWindow.WindowState}'.");
if (showSingleInstanceNotice)
{
lanMountainMainWindow.ShowSingleInstanceNotice();
mainWindow.ShowSingleInstanceNotice();
}
}
catch (Exception ex)
{
AppLogger.Warn("SingleInstance", "Failed to activate the existing main window.", ex);
AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex);
}
}, DispatcherPriority.Send);
}
internal void PrepareForShutdown(bool isRestart, string source)
{
void Mark()
{
_shutdownIntent = isRestart
? ShutdownIntent.RestartRequested
: ShutdownIntent.ExitRequested;
AppLogger.Info(
"DesktopShell",
$"Shutdown intent marked. Intent='{_shutdownIntent}'; Source='{source}'; CurrentShellState='{_desktopShellState}'.");
}
if (Dispatcher.UIThread.CheckAccess())
{
Mark();
return;
}
Dispatcher.UIThread.InvokeAsync(Mark, DispatcherPriority.Send).GetAwaiter().GetResult();
}
internal void ResetShutdownIntent(string source)
{
void Reset()
{
if (_shutdownIntent == ShutdownIntent.None)
{
return;
}
AppLogger.Warn(
"DesktopShell",
$"Shutdown intent cleared without process exit. PreviousIntent='{_shutdownIntent}'; Source='{source}'.");
_shutdownIntent = ShutdownIntent.None;
}
if (Dispatcher.UIThread.CheckAccess())
{
Reset();
return;
}
Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
}
private void OnAppSettingsSaved(string _)
{
Dispatcher.UIThread.Post(() =>
@@ -311,18 +361,7 @@ public partial class App : Application
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
try
{
_traySettingsWindow?.Close();
}
catch (Exception ex)
{
AppLogger.Warn("App", "Failed to close tray-opened settings window during shutdown.", ex);
}
finally
{
_traySettingsWindow = null;
}
_independentSettingsModuleService.CloseIfOpen();
try
{
@@ -342,6 +381,171 @@ public partial class App : Application
DisposeTrayIcon();
}
private MainWindow CreateAndAssignMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
var mainWindow = new MainWindow
{
DataContext = new MainWindowViewModel(),
ShowInTaskbar = true
};
AttachMainWindow(mainWindow);
desktop.MainWindow = mainWindow;
AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
return mainWindow;
}
private MainWindow GetOrCreateMainWindow(
IClassicDesktopStyleApplicationLifetime desktop,
string reason)
{
if (_mainWindow is not null && !_mainWindowClosed)
{
return _mainWindow;
}
if (desktop.MainWindow is MainWindow desktopMainWindow && !_mainWindowClosed)
{
AttachMainWindow(desktopMainWindow);
return desktopMainWindow;
}
return CreateAndAssignMainWindow(desktop, reason);
}
private void AttachMainWindow(MainWindow mainWindow)
{
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindowClosed = false;
return;
}
if (_mainWindow is not null)
{
_mainWindow.Closing -= OnMainWindowClosing;
_mainWindow.Closed -= OnMainWindowClosed;
_mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
}
_mainWindow = mainWindow;
_mainWindowClosed = false;
mainWindow.Closing += OnMainWindowClosing;
mainWindow.Closed += OnMainWindowClosed;
mainWindow.PropertyChanged += OnMainWindowPropertyChanged;
}
private void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
AppLogger.Info(
"DesktopShell",
$"Main window closing requested. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'; WindowState='{mainWindow.WindowState}'; IsVisible={mainWindow.IsVisible}.");
if (_shutdownIntent is ShutdownIntent.ExitRequested or ShutdownIntent.RestartRequested)
{
AppLogger.Info(
"DesktopShell",
$"Main window close allowed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
return;
}
e.Cancel = true;
HideMainWindowToTray(mainWindow, "MainWindowClosing");
}
private void OnMainWindowClosed(object? sender, EventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
mainWindow.Closing -= OnMainWindowClosing;
mainWindow.Closed -= OnMainWindowClosed;
mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
if (ReferenceEquals(_mainWindow, mainWindow))
{
_mainWindow = null;
}
_mainWindowClosed = true;
AppLogger.Info(
"DesktopShell",
$"Main window closed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
if (_shutdownIntent == ShutdownIntent.None)
{
SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
}
}
private void OnMainWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (sender is not MainWindow mainWindow)
{
return;
}
if (e.Property != Window.WindowStateProperty)
{
return;
}
if (_shutdownIntent != ShutdownIntent.None || !mainWindow.IsVisible)
{
return;
}
if (mainWindow.WindowState == WindowState.Minimized)
{
SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "MainWindowMinimized");
return;
}
SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowRestored");
}
private void HideMainWindowToTray(MainWindow mainWindow, string source)
{
try
{
mainWindow.ShowInTaskbar = false;
mainWindow.Hide();
SetDesktopShellState(DesktopShellState.TrayOnly, source);
AppLogger.Info(
"DesktopShell",
$"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'.");
}
catch (Exception ex)
{
AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex);
}
}
private void SetDesktopShellState(DesktopShellState state, string source)
{
if (_desktopShellState == state)
{
return;
}
var previous = _desktopShellState;
_desktopShellState = state;
AppLogger.Info(
"DesktopShell",
$"Shell state changed. Previous='{previous}'; Current='{state}'; Source='{source}'.");
}
private void LogBrowserStartupDiagnostics()
{
try

View File

@@ -1,6 +1,7 @@
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
"tray.menu.show_desktop": "Open Desktop",
"tray.menu.settings": "Settings",
"tray.menu.restart": "Restart App",
"tray.menu.exit": "Exit App",
@@ -8,10 +9,10 @@
"tooltip.back_to_windows": "Back to Windows",
"tooltip.open_settings": "Settings",
"settings.title": "Settings",
"settings.shell.title": "Application Settings",
"settings.shell.subtitle": "LanMountainDesktop standalone preferences",
"settings.shell.title": "Settings",
"settings.shell.subtitle": "LanMountainDesktop independent settings module",
"settings.shell.sidebar_hint": "Choose a category to adjust application behavior, desktop layout, and appearance.",
"settings.shell.footer_hint": "Tray-opened settings are managed in this standalone window.",
"settings.shell.footer_hint": "Tray-opened settings are managed in this independent settings module.",
"settings.back_to_desktop": "Back to Desktop",
"settings.nav_header": "Settings",
"settings.nav.group_desktop": "Desktop",

View File

@@ -1,6 +1,7 @@
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
"tray.menu.show_desktop": "打开桌面",
"tray.menu.settings": "设置",
"tray.menu.restart": "重启应用",
"tray.menu.exit": "退出应用",
@@ -8,10 +9,10 @@
"tooltip.back_to_windows": "回到Windows",
"tooltip.open_settings": "设置",
"settings.title": "设置",
"settings.shell.title": "应用设置",
"settings.shell.subtitle": "LanMountainDesktop 独立设置窗口",
"settings.shell.title": "设置",
"settings.shell.subtitle": "LanMountainDesktop 独立设置模块",
"settings.shell.sidebar_hint": "选择一个分类以调整应用行为、桌面布局与外观。",
"settings.shell.footer_hint": "托盘菜单打开的设置会统一在这个独立窗口中管理。",
"settings.shell.footer_hint": "托盘菜单打开的设置会统一在这个独立设置模块中管理。",
"settings.back_to_desktop": "返回桌面",
"settings.nav_header": "设置选项",
"settings.nav.group_desktop": "桌面",

View File

@@ -11,18 +11,21 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
{
public bool TryExit(HostApplicationLifecycleRequest? request = null)
{
App? app = null;
try
{
AppLogger.Info(
"HostLifecycle",
$"Exit requested. Source='{request?.Source ?? "Unknown"}'; Reason='{request?.Reason ?? string.Empty}'.");
if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
app = Application.Current as App;
if (app?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn("HostLifecycle", "Exit request ignored because desktop lifetime is unavailable.");
return false;
}
app.PrepareForShutdown(isRestart: false, request?.Source ?? "Unknown");
if (Dispatcher.UIThread.CheckAccess())
{
desktop.Shutdown();
@@ -36,6 +39,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
catch (Exception ex)
{
app?.ResetShutdownIntent(request?.Source ?? "Unknown");
AppLogger.Warn("HostLifecycle", "Failed to exit the application.", ex);
return false;
}
@@ -43,6 +47,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
public bool TryRestart(HostApplicationLifecycleRequest? request = null)
{
App? app = null;
try
{
var startInfo = AppRestartService.CreateRestartStartInfo();
@@ -55,6 +60,8 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
Process.Start(startInfo);
app = Application.Current as App;
app?.PrepareForShutdown(isRestart: true, request?.Source ?? "Unknown");
var exitRequest = request is null
? new HostApplicationLifecycleRequest(Reason: "Restart accepted.")
: request with
@@ -68,6 +75,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
catch (Exception ex)
{
app?.ResetShutdownIntent(request?.Source ?? "Unknown");
AppLogger.Warn("HostLifecycle", "Failed to restart the application.", ex);
return false;
}

View File

@@ -0,0 +1,88 @@
using System;
using Avalonia.Threading;
using LanMountainDesktop.Views;
namespace LanMountainDesktop.Services;
internal sealed class IndependentSettingsModuleService
{
private SettingsWindow? _window;
public void ShowOrActivate(string source, string? pageTag = null)
{
AppLogger.Info("IndependentSettingsModule", $"OpenRequested; Source='{source}'; PageTag='{pageTag ?? "<default>"}'.");
void ShowCore()
{
try
{
if (_window is not { } window)
{
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionStarted; Source='{source}'.");
window = new SettingsWindow();
AppLogger.Info("IndependentSettingsModule", $"WindowConstructionCompleted; Source='{source}'.");
window.Closed += (_, _) =>
{
if (ReferenceEquals(_window, window))
{
_window = null;
}
AppLogger.Info("IndependentSettingsModule", "WindowClosed.");
};
_window = window;
}
window.Open(pageTag);
AppLogger.Info(
"IndependentSettingsModule",
$"WindowActivated; Source='{source}'; ReusedExisting={ReferenceEquals(_window, window)}; WasVisible={window.IsVisible}; PageTag='{pageTag ?? "<default>"}'.");
}
catch (Exception ex)
{
AppLogger.Warn("IndependentSettingsModule", $"Failed to open independent settings module window. Source='{source}'.", ex);
}
}
if (Dispatcher.UIThread.CheckAccess())
{
ShowCore();
return;
}
Dispatcher.UIThread.Post(ShowCore, DispatcherPriority.Normal);
}
public void CloseIfOpen()
{
void CloseCore()
{
if (_window is null)
{
return;
}
try
{
_window.PrepareForForceClose();
_window.Close();
}
catch (Exception ex)
{
AppLogger.Warn("IndependentSettingsModule", "Failed to close independent settings module window during shutdown.", ex);
}
finally
{
_window = null;
}
}
if (Dispatcher.UIThread.CheckAccess())
{
CloseCore();
return;
}
Dispatcher.UIThread.Post(CloseCore, DispatcherPriority.Send);
}
}

View File

@@ -0,0 +1,23 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using FluentAvalonia.UI.Windowing;
namespace LanMountainDesktop.Views;
public class IndependentSettingsModuleWindowBase : AppWindow
{
public IndependentSettingsModuleWindowBase()
{
TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
TitleBar.Height = 48;
if (OperatingSystem.IsWindows())
{
TransparencyLevelHint = [WindowTransparencyLevel.Mica];
Background = Brushes.Transparent;
}
}
}

View File

@@ -0,0 +1,9 @@
namespace LanMountainDesktop.Views;
internal enum IndependentSettingsPageCategory
{
Internal = 0,
External = 1,
About = 2,
Debug = 3
}

View File

@@ -0,0 +1,12 @@
using FluentIcons.Common;
namespace LanMountainDesktop.Views;
internal sealed record IndependentSettingsPageDefinition(
string Tag,
string Title,
string Description,
Symbol Icon,
IndependentSettingsPageCategory Category,
int SortOrder,
string? ToolTip = null);

View File

@@ -410,7 +410,10 @@ public partial class MainWindow
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
OpenSettingsPage();
if (Application.Current is App app)
{
app.OpenIndependentSettingsModule("ComponentLibrary");
}
}
}, FluttermotionToken.Slow);
}

View File

@@ -80,11 +80,13 @@ public partial class MainWindow
if (_isSettingsOpen)
{
CloseSettingsPage();
return;
CloseSettingsPage(immediate: true);
}
OpenSettingsPage();
if (Application.Current is App app)
{
app.OpenIndependentSettingsModule("MainWindow");
}
}
private void OnCloseSettingsClick(object? sender, RoutedEventArgs e)

View File

@@ -0,0 +1,3 @@
namespace LanMountainDesktop.Views;
internal sealed record SettingsComponentCategorySummary(string Name, string CountText);

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -8,16 +8,16 @@
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage">
<StackPanel x:Name="AboutSettingsPanel" Spacing="20">
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Text="About" />
<TextBlock x:Name="AboutPanelTitleTextBlock" FontSize="24" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" Text="About" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="20">
<StackPanel Spacing="12">
<TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" />
<TextBlock x:Name="VersionTextBlock" Text="Version: 1.0.0" FontSize="13" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="VersionTextBlock" Text="Version: 1.0.0" FontSize="13" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CodeNameTextBlock" Text="Code Name: Administrate" FontSize="13" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveAccentBrush}" />
<TextBlock x:Name="FontInfoTextBlock" Text="Font: MiSans" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock x:Name="FontInfoTextBlock" Text="Font: MiSans" FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Border>
@@ -49,17 +49,17 @@
Text="Current actual backend"
FontSize="12"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendValueTextBlock"
Text="Current backend: Software"
FontSize="13"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendImplementationTextBlock"
Text="Runtime implementation is unavailable."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
<ui:SettingsExpander.Footer>
<ComboBox x:Name="AppRenderModeComboBox"
@@ -77,3 +77,4 @@
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1400"
x:Class="LanMountainDesktop.Views.SettingsPages.AppearanceSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="AppearancePageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Personalize wallpaper, desktop grid, and accent colors in one place." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceWallpaperSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Wallpaper" />
<TextBlock x:Name="AppearanceWallpaperSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Use lightweight thumbnails and asset controls instead of heavy live preview." />
<ContentControl x:Name="WallpaperContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceGridSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Grid" />
<TextBlock x:Name="AppearanceGridSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Tune grid density, spacing, and safe edge inset for the desktop canvas." />
<ContentControl x:Name="GridContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="AppearanceColorSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Color" />
<TextBlock x:Name="AppearanceColorSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Choose theme mode and accent colors with Fluent-consistent swatches." />
<ContentControl x:Name="ColorContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class AppearanceSettingsPage : UserControl
{
public AppearanceSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -8,19 +8,27 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.ColorSettingsPage">
<StackPanel x:Name="ColorSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="ColorPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Color" />
<TextBlock x:Name="ColorPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Adjust theme mode and accent colors. The desktop shell will reuse these colors consistently." />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
Header="&#26085;&#22812;&#27169;&#24335;"
Description="&#20999;&#25442;&#24212;&#29992;&#30340;&#27973;&#33394;&#25110;&#28145;&#33394;&#20027;&#39064;&#12290;">
<ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="DarkTheme"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="NightModeToggleSwitch"
@@ -35,14 +43,15 @@
Header="&#20027;&#39064;&#33394;"
Description="&#36873;&#25321;&#24212;&#29992;&#30340;&#20027;&#39064;&#28857;&#32512;&#33394;&#12290;">
<ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="Color"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="12">
<TextBlock x:Name="RecommendedColorsLabelTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Recommended Colors" />
<WrapPanel ItemWidth="72"
ItemHeight="56"
@@ -119,7 +128,7 @@
ColumnSpacing="10">
<TextBlock x:Name="SystemMonetColorsLabelTextBlock"
VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="System Monet Colors" />
<Button x:Name="RefreshMonetColorsButton"
Grid.Column="1"
@@ -197,8 +206,13 @@
</ui:SettingsExpander>
</Border>
<TextBlock x:Name="ThemeColorStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
Text="Theme color is ready." />
<Border Classes="settings-expander-shell"
Padding="16,14">
<TextBlock x:Name="ThemeColorStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextMutedBrush}"
TextWrapping="Wrap"
Text="Theme color is ready." />
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,48 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1200"
x:Class="LanMountainDesktop.Views.SettingsPages.ComponentsSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="ComponentsPageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Review available desktop components and configure the status bar area." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="ComponentsSummarySectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Component Library" />
<TextBlock x:Name="ComponentsSummarySectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Built-in and plugin-contributed components available to the desktop editor." />
<TextBlock x:Name="ComponentsSummaryTextBlock"
TextWrapping="Wrap"
Text="Loading component catalog..." />
<StackPanel x:Name="ComponentCategoryItemsPanel"
Spacing="4" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="ComponentsStatusBarSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Status Bar" />
<TextBlock x:Name="ComponentsStatusBarSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Clock and status-bar component spacing are managed here." />
<ContentControl x:Name="StatusBarContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class ComponentsSettingsPage : UserControl
{
public ComponentsSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -0,0 +1,58 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1400"
x:Class="LanMountainDesktop.Views.SettingsPages.GeneralSettingsPage">
<StackPanel MaxWidth="920"
Spacing="16">
<TextBlock x:Name="GeneralPageSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Manage language, launcher, and weather behavior from the independent settings module." />
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralRegionSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Region" />
<TextBlock x:Name="GeneralRegionSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Language and time zone settings affect the entire desktop shell." />
<ContentControl x:Name="RegionContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralLauncherSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Launcher" />
<TextBlock x:Name="GeneralLauncherSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Restore hidden entries and adjust how the app launcher behaves." />
<ContentControl x:Name="LauncherContentHost" />
</StackPanel>
</Border>
<Border Classes="settings-page-shell">
<StackPanel Spacing="12">
<TextBlock x:Name="GeneralWeatherSectionTitleTextBlock"
FontSize="18"
FontWeight="SemiBold"
Text="Weather" />
<TextBlock x:Name="GeneralWeatherSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Configure shared weather source, location, and icon style for weather widgets." />
<ContentControl x:Name="WeatherContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace LanMountainDesktop.Views.SettingsPages;
public partial class GeneralSettingsPage : UserControl
{
public GeneralSettingsPage()
{
InitializeComponent();
}
}

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,30 +9,32 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.GridSettingsPage">
<Grid x:Name="GridSettingsPanel"
ColumnDefinitions="*, *"
ColumnDefinitions="280, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="GridPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,24"
Text="调整网格布局" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,20"
Text="璋冩暣缃戞牸甯冨眬" />
<!-- Left Column: Grid Preview -->
<Border x:Name="GridPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0"
Margin="0,0,20,0"
Width="256"
MaxWidth="256"
VerticalAlignment="Top"
HorizontalAlignment="Stretch">
HorizontalAlignment="Left">
<Border x:Name="GridPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="28"
CornerRadius="22"
Background="#FF1A1A1A"
Padding="12">
Padding="8">
<Border x:Name="GridPreviewViewport"
ClipToBounds="True"
CornerRadius="16"
CornerRadius="14"
Background="#30111827">
<Panel>
<Canvas x:Name="GridPreviewLinesCanvas"
@@ -63,7 +65,7 @@
<Border x:Name="GridPreviewTaskbarFixedActionsHost" Grid.Column="0">
<StackPanel x:Name="GridPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
<fi:SymbolIcon Classes="icon-s" Symbol="Window" />
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="回到Windows" VerticalAlignment="Center" />
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="鍥炲埌Windows" VerticalAlignment="Center" />
</StackPanel>
</Border>
<StackPanel x:Name="GridPreviewTaskbarDynamicActionsHost"
@@ -90,7 +92,7 @@
<!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1"
Margin="16,0,0,0"
Margin="20,0,0,0"
Spacing="16">
<Border Classes="settings-expander-shell">
@@ -109,7 +111,7 @@
Width="80"
Minimum="6"
Maximum="96"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="12" />
</Grid>
</ui:SettingsExpander.Footer>
@@ -144,14 +146,14 @@
Width="80"
Minimum="0"
Maximum="30"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="18" />
</Grid>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
@@ -161,12 +163,13 @@
<Button x:Name="ApplyGridButton"
HorizontalAlignment="Stretch"
Padding="0,10"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Content="应用" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Content="搴旂敤" />
<TextBlock x:Name="GridInfoTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Grid: - cols x - rows (1:1)" />
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<TextBlock x:Name="LauncherSettingsPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="App Launcher" />
<Border Classes="settings-expander-shell">
@@ -25,11 +25,11 @@
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="LauncherHiddenItemsDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Right-click an icon in launcher to hide it. Hidden entries appear here." />
<TextBlock x:Name="LauncherHiddenItemsEmptyTextBlock"
IsVisible="False"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No hidden items." />
</StackPanel>
</ui:SettingsExpander.Footer>
@@ -37,3 +37,4 @@
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,7 +12,7 @@
<TextBlock x:Name="RegionPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Region" />
<Border Classes="settings-expander-shell">
@@ -25,7 +25,7 @@
<ui:SettingsExpander.Footer>
<ComboBox x:Name="LanguageComboBox"
Width="220">
<ComboBoxItem x:Name="LanguageChineseItem" Tag="zh-CN" Content="中文" />
<ComboBoxItem x:Name="LanguageChineseItem" Tag="zh-CN" Content="涓枃" />
<ComboBoxItem x:Name="LanguageEnglishItem" Tag="en-US" Content="English" />
</ComboBox>
</ui:SettingsExpander.Footer>
@@ -47,3 +47,4 @@
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<TextBlock x:Name="StatusBarPanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Status Bar" />
<Border Classes="settings-expander-shell">
@@ -80,7 +80,7 @@
<ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
HorizontalAlignment="Right"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem>
@@ -88,3 +88,4 @@
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,7 +12,7 @@
<TextBlock x:Name="UpdatePanelTitleTextBlock"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Update" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
@@ -22,31 +22,31 @@
<TextBlock x:Name="UpdateCurrentVersionLabelTextBlock"
Text="Current Version"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateCurrentVersionValueTextBlock"
Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionLabelTextBlock"
Grid.Row="1"
Text="Latest Release"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionValueTextBlock"
Grid.Row="1" Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtLabelTextBlock"
Grid.Row="2"
Text="Published At"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtValueTextBlock"
Grid.Row="2" Grid.Column="1"
Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</Grid>
</Border>
@@ -65,7 +65,7 @@
<TextBlock x:Name="UpdateChannelLabelTextBlock"
Text="Update Channel"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ListBox x:Name="UpdateChannelChipListBox"
Classes="settings-chip-list">
<ListBox.ItemsPanel>
@@ -110,13 +110,14 @@
IsVisible="False" />
<TextBlock x:Name="UpdateDownloadProgressTextBlock"
Text="Download progress: -"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateStatusTextBlock"
Text="Ready to check for updates."
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</Border>
</StackPanel>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,31 +9,33 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage">
<Grid x:Name="WallpaperSettingsPanel"
ColumnDefinitions="*, *"
ColumnDefinitions="280, *"
RowDefinitions="Auto, *">
<TextBlock x:Name="WallpaperPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Margin="0,0,0,24"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,20"
Text="Personalize Wallpaper" />
<!-- Left Column: Monitor Preview -->
<Border x:Name="WallpaperPreviewHost"
Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0"
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch">
Margin="0,0,20,0"
Width="256"
MaxWidth="256"
VerticalAlignment="Top"
HorizontalAlignment="Left">
<!-- Monitor Frame (Bezel) -->
<Border x:Name="WallpaperPreviewFrame"
HorizontalAlignment="Stretch"
CornerRadius="28"
CornerRadius="22"
Background="#FF1A1A1A"
Padding="12">
Padding="8">
<Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True"
CornerRadius="12"
CornerRadius="14"
Background="#30111827">
<Grid>
<Image x:Name="WallpaperPreviewVideoImage"
@@ -96,14 +98,14 @@
<!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1"
Margin="16,0,0,0"
Margin="20,0,0,0"
Spacing="16">
<StackPanel Spacing="8">
<TextBlock Text="Preview status" FontSize="12" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<TextBlock Text="Preview status" FontSize="12" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="WallpaperPathTextBlock"
FontSize="14"
FontWeight="Medium"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis"
Text="No file selected" />
<TextBlock x:Name="WallpaperStatusTextBlock"
@@ -114,7 +116,7 @@
<Separator Background="{DynamicResource SurfaceStrokeColorDefaultBrush}" Height="1" Margin="0,8" />
<TextBlock Text="Choose image or video" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock Text="Choose image or video" FontSize="16" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<Button x:Name="PickWallpaperButton"
@@ -149,3 +151,4 @@
</StackPanel>
</Grid>
</UserControl>

View File

@@ -1,4 +1,4 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<Style Selector="StackPanel.weather-settings-root TextBlock.section-eyebrow">
<Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Style>
<Style Selector="StackPanel.weather-settings-root Border.preview-icon-shell">
@@ -42,7 +42,7 @@
<TextBlock x:Name="WeatherPanelTitleTextBlock"
FontSize="28"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Weather" />
<StackPanel Spacing="8">
@@ -70,7 +70,7 @@
Text="--" />
<TextBlock x:Name="WeatherPreviewUpdatedTextBlock"
FontSize="13"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="-" />
</StackPanel>
@@ -92,7 +92,7 @@
<TextBlock x:Name="WeatherPreviewResultTextBlock"
Grid.Row="1"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Use refresh to verify your weather configuration." />
</Grid>
</Border>
@@ -138,7 +138,7 @@
FontWeight="SemiBold"
Text="City Selection" />
<TextBlock x:Name="WeatherLocationSelectionDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Select the current city used for weather queries." />
</StackPanel>
@@ -157,7 +157,7 @@
FontSize="12"
TextAlignment="Right"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</Grid>
</ui:SettingsExpanderItem>
@@ -216,7 +216,7 @@
<TextBlock x:Name="WeatherSearchStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Search by city name and apply one location." />
</StackPanel>
@@ -266,7 +266,7 @@
Watermark="Display name (optional)" />
<TextBlock x:Name="WeatherCoordinateStatusTextBlock"
FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" />
</StackPanel>
</ui:SettingsExpanderItem>
@@ -288,7 +288,7 @@
FontWeight="SemiBold"
Text="Exclude List" />
<TextBlock x:Name="WeatherAlertListDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="One exclusion rule per line." />
</StackPanel>
@@ -321,7 +321,7 @@
<Border Classes="settings-note-shell">
<TextBlock x:Name="WeatherFooterHintTextBlock"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Desktop weather widgets will reuse the location and alert exclusion settings configured here." />
</Border>
@@ -352,3 +352,4 @@
</Grid>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,78 @@
using System;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private void UpdateComponentsSettingsSummary()
{
if (ComponentsSettingsHubPanel is null)
{
return;
}
var definitions = _componentRegistry
.GetAll()
.OrderBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
.ThenBy(definition => definition.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToList();
var runtime = (Application.Current as App)?.PluginRuntimeService;
var pluginComponentIds = runtime?.DesktopComponents
.Select(contribution => contribution.Registration.ComponentId)
.ToHashSet(StringComparer.OrdinalIgnoreCase) ?? [];
var pluginCount = definitions.Count(definition => pluginComponentIds.Contains(definition.Id));
var builtInCount = definitions.Count - pluginCount;
var desktopCount = definitions.Count(definition => definition.AllowDesktopPlacement);
var statusBarCount = definitions.Count(definition => definition.AllowStatusBarPlacement);
ComponentsSettingsHubPanel.ComponentsSummaryTextBlock.Text = Lf(
"settings.components.summary_format",
"Available components: {0}. Built-in: {1}. Plugin-provided: {2}. Desktop: {3}. Status bar: {4}.",
definitions.Count,
builtInCount,
pluginCount,
desktopCount,
statusBarCount);
ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Clear();
foreach (var group in definitions
.GroupBy(definition => definition.Category, StringComparer.OrdinalIgnoreCase)
.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase))
{
ComponentsSettingsHubPanel.ComponentCategoryItemsPanel.Children.Add(new Border
{
Margin = new Thickness(0, 4, 0, 0),
Padding = new Thickness(12, 10),
Background = GetThemeBrush("LayerFillColorDefaultBrush"),
BorderBrush = GetThemeBrush("CardStrokeColorDefaultBrush"),
BorderThickness = new Thickness(1),
CornerRadius = new CornerRadius(12),
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 8,
Children =
{
new TextBlock
{
FontWeight = FontWeight.SemiBold,
Text = group.Key
},
new TextBlock
{
Foreground = GetThemeBrush("TextFillColorSecondaryBrush"),
Text = Lf("settings.components.category_count_format", "{0} item(s)", group.Count())
}
}
}
});
}
}
}

View File

@@ -1,217 +1,261 @@
using Avalonia.Controls;
using System;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
// --- WallpaperSettingsPage ---
internal TextBlock WallpaperPanelTitleTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPanelTitleTextBlock")!;
internal TextBlock WallpaperPathTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPathTextBlock")!;
internal TextBlock WallpaperStatusTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperStatusTextBlock")!;
internal ComboBox WallpaperPlacementComboBox => WallpaperSettingsPanel.FindControl<ComboBox>("WallpaperPlacementComboBox")!;
internal Border WallpaperPreviewHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewHost")!;
internal Border WallpaperPreviewFrame => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewFrame")!;
internal Border WallpaperPreviewViewport => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewViewport")!;
internal Image WallpaperPreviewVideoImage => WallpaperSettingsPanel.FindControl<Image>("WallpaperPreviewVideoImage")!;
internal Grid WallpaperPreviewGrid => WallpaperSettingsPanel.FindControl<Grid>("WallpaperPreviewGrid")!;
internal Border WallpaperPreviewTopStatusBarHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTopStatusBarHost")!;
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel")!;
internal LanMountainDesktop.Views.Components.ClockWidget WallpaperPreviewClockWidget => WallpaperSettingsPanel.FindControl<LanMountainDesktop.Views.Components.ClockWidget>("WallpaperPreviewClockWidget")!;
internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewBottomTaskbarContainer")!;
internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarFixedActionsHost")!;
internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewBackButtonVisual")!;
internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewBackButtonTextBlock")!;
internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewTaskbarDynamicActionsHost")!;
internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperSettingsPanel.FindControl<Border>("WallpaperPreviewTaskbarSettingsActionHost")!;
internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperSettingsPanel.FindControl<StackPanel>("WallpaperPreviewComponentLibraryVisual")!;
internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperSettingsPanel.FindControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon")!;
internal Button PickWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("PickWallpaperButton")!;
internal Button ClearWallpaperButton => WallpaperSettingsPanel.FindControl<Button>("ClearWallpaperButton")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WallpaperPlacementSettingsExpander => WallpaperSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WallpaperPlacementSettingsExpander")!;
private string CurrentControlAccessStage => _controlsBound ? "module init" : "control binding";
// --- GridSettingsPage ---
internal TextBlock GridPanelTitleTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPanelTitleTextBlock")!;
internal Border GridPreviewHost => GridSettingsPanel.FindControl<Border>("GridPreviewHost")!;
internal Border GridPreviewFrame => GridSettingsPanel.FindControl<Border>("GridPreviewFrame")!;
internal Border GridPreviewViewport => GridSettingsPanel.FindControl<Border>("GridPreviewViewport")!;
internal Canvas GridPreviewLinesCanvas => GridSettingsPanel.FindControl<Canvas>("GridPreviewLinesCanvas")!;
internal Grid GridPreviewGrid => GridSettingsPanel.FindControl<Grid>("GridPreviewGrid")!;
internal Border GridPreviewTopStatusBarHost => GridSettingsPanel.FindControl<Border>("GridPreviewTopStatusBarHost")!;
internal StackPanel GridPreviewTopStatusComponentsPanel => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTopStatusComponentsPanel")!;
internal Border GridPreviewBottomTaskbarContainer => GridSettingsPanel.FindControl<Border>("GridPreviewBottomTaskbarContainer")!;
internal Border GridPreviewTaskbarFixedActionsHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarFixedActionsHost")!;
internal StackPanel GridPreviewBackButtonVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewBackButtonVisual")!;
internal TextBlock GridPreviewBackButtonTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewBackButtonTextBlock")!;
internal StackPanel GridPreviewTaskbarDynamicActionsHost => GridSettingsPanel.FindControl<StackPanel>("GridPreviewTaskbarDynamicActionsHost")!;
internal Border GridPreviewTaskbarSettingsActionHost => GridSettingsPanel.FindControl<Border>("GridPreviewTaskbarSettingsActionHost")!;
internal StackPanel GridPreviewComponentLibraryVisual => GridSettingsPanel.FindControl<StackPanel>("GridPreviewComponentLibraryVisual")!;
internal FluentIcons.Avalonia.FluentIcon GridPreviewComponentLibraryIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon")!;
internal TextBlock GridPreviewComponentLibraryTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPreviewComponentLibraryTextBlock")!;
internal FluentIcons.Avalonia.SymbolIcon GridPreviewSettingsButtonIcon => GridSettingsPanel.FindControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridRowsSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridRowsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridSpacingSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridSpacingSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander GridEdgeInsetSettingsExpander => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridEdgeInsetSettingsExpander")!;
internal Slider GridSizeSlider => GridSettingsPanel.FindControl<Slider>("GridSizeSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridSizeNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridSizeNumberBox")!;
internal ComboBox GridSpacingPresetComboBox => GridSettingsPanel.FindControl<ComboBox>("GridSpacingPresetComboBox")!;
internal ComboBoxItem GridSpacingRelaxedComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingRelaxedComboBoxItem")!;
internal ComboBoxItem GridSpacingCompactComboBoxItem => GridSettingsPanel.FindControl<ComboBoxItem>("GridSpacingCompactComboBoxItem")!;
internal Slider GridEdgeInsetSlider => GridSettingsPanel.FindControl<Slider>("GridEdgeInsetSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox GridEdgeInsetNumberBox => GridSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("GridEdgeInsetNumberBox")!;
internal TextBlock GridEdgeInsetComputedPxTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridEdgeInsetComputedPxTextBlock")!;
internal Button ApplyGridButton => GridSettingsPanel.FindControl<Button>("ApplyGridButton")!;
internal TextBlock GridInfoTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridInfoTextBlock")!;
private T? TryGetOptionalPageControl<T>(Control? pageRoot, string controlName)
where T : Control
{
return pageRoot?.FindControl<T>(controlName);
}
// --- ColorSettingsPage ---
internal TextBlock ColorPanelTitleTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ColorPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeModeSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeModeSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeColorSettingsExpander => ColorSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeColorSettingsExpander")!;
internal ToggleSwitch NightModeToggleSwitch => ColorSettingsPanel.FindControl<ToggleSwitch>("NightModeToggleSwitch")!;
internal TextBlock ThemeColorStatusTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ThemeColorStatusTextBlock")!;
internal TextBlock RecommendedColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("RecommendedColorsLabelTextBlock")!;
internal TextBlock SystemMonetColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("SystemMonetColorsLabelTextBlock")!;
internal Button RecommendedColorButton1 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton1")!;
internal Button RecommendedColorButton2 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton2")!;
internal Button RecommendedColorButton3 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton3")!;
internal Button RecommendedColorButton4 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton4")!;
internal Button RecommendedColorButton5 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton5")!;
internal Button RecommendedColorButton6 => ColorSettingsPanel.FindControl<Button>("RecommendedColorButton6")!;
internal Border RecommendedColorSwatch1 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch1")!;
internal Border RecommendedColorSwatch2 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch2")!;
internal Border RecommendedColorSwatch3 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch3")!;
internal Border RecommendedColorSwatch4 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch4")!;
internal Border RecommendedColorSwatch5 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch5")!;
internal Border RecommendedColorSwatch6 => ColorSettingsPanel.FindControl<Border>("RecommendedColorSwatch6")!;
internal Button RefreshMonetColorsButton => ColorSettingsPanel.FindControl<Button>("RefreshMonetColorsButton")!;
internal Button MonetColorButton1 => ColorSettingsPanel.FindControl<Button>("MonetColorButton1")!;
internal Button MonetColorButton2 => ColorSettingsPanel.FindControl<Button>("MonetColorButton2")!;
internal Button MonetColorButton3 => ColorSettingsPanel.FindControl<Button>("MonetColorButton3")!;
internal Button MonetColorButton4 => ColorSettingsPanel.FindControl<Button>("MonetColorButton4")!;
internal Button MonetColorButton5 => ColorSettingsPanel.FindControl<Button>("MonetColorButton5")!;
internal Button MonetColorButton6 => ColorSettingsPanel.FindControl<Button>("MonetColorButton6")!;
internal Border MonetColorSwatch1 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch1")!;
internal Border MonetColorSwatch2 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch2")!;
internal Border MonetColorSwatch3 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch3")!;
internal Border MonetColorSwatch4 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch4")!;
internal Border MonetColorSwatch5 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch5")!;
internal Border MonetColorSwatch6 => ColorSettingsPanel.FindControl<Border>("MonetColorSwatch6")!;
private T RequirePageControl<T>(Control pageRoot, string controlName)
where T : Control
{
var control = pageRoot.FindControl<T>(controlName);
if (control is null)
{
throw new InvalidOperationException(
$"Independent settings module control resolution failed. Page='{pageRoot.Name ?? pageRoot.GetType().Name}'; Control='{controlName}'; Stage='{CurrentControlAccessStage}'.");
}
// --- StatusBarSettingsPage ---
internal TextBlock StatusBarPanelTitleTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarClockSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarClockSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarSpacingSettingsExpander => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarSpacingSettingsExpander")!;
internal ToggleSwitch StatusBarClockToggleSwitch => StatusBarSettingsPanel.FindControl<ToggleSwitch>("StatusBarClockToggleSwitch")!;
internal RadioButton ClockFormatHMSSRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMSSRadio")!;
internal RadioButton ClockFormatHMRadio => StatusBarSettingsPanel.FindControl<RadioButton>("ClockFormatHMRadio")!;
internal ComboBox StatusBarSpacingModeComboBox => StatusBarSettingsPanel.FindControl<ComboBox>("StatusBarSpacingModeComboBox")!;
internal ComboBoxItem StatusBarSpacingModeCompactItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCompactItem")!;
internal ComboBoxItem StatusBarSpacingModeRelaxedItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeRelaxedItem")!;
internal ComboBoxItem StatusBarSpacingModeCustomItem => StatusBarSettingsPanel.FindControl<ComboBoxItem>("StatusBarSpacingModeCustomItem")!;
internal FluentAvalonia.UI.Controls.SettingsExpanderItem StatusBarSpacingCustomPanel => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpanderItem>("StatusBarSpacingCustomPanel")!;
internal Slider StatusBarSpacingSlider => StatusBarSettingsPanel.FindControl<Slider>("StatusBarSpacingSlider")!;
internal FluentAvalonia.UI.Controls.NumberBox StatusBarSpacingNumberBox => StatusBarSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("StatusBarSpacingNumberBox")!;
internal TextBlock StatusBarSpacingComputedPxTextBlock => StatusBarSettingsPanel.FindControl<TextBlock>("StatusBarSpacingComputedPxTextBlock")!;
return control;
}
// --- RegionSettingsPage ---
internal TextBlock RegionPanelTitleTextBlock => RegionSettingsPanel.FindControl<TextBlock>("RegionPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LanguageSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LanguageSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander TimeZoneSettingsExpander => RegionSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("TimeZoneSettingsExpander")!;
internal ComboBox LanguageComboBox => RegionSettingsPanel.FindControl<ComboBox>("LanguageComboBox")!;
internal ComboBoxItem LanguageChineseItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageChineseItem")!;
internal ComboBoxItem LanguageEnglishItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageEnglishItem")!;
internal ComboBox TimeZoneComboBox => RegionSettingsPanel.FindControl<ComboBox>("TimeZoneComboBox")!;
private T RequireSettingsPage<T>(T? page, string pageName)
where T : Control
{
return page ?? throw new InvalidOperationException(
$"Independent settings module page resolution failed. Page='{pageName}'; Stage='{CurrentControlAccessStage}'.");
}
// --- WeatherSettingsPage ---
internal TextBlock WeatherPanelTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPanelTitleTextBlock")!;
internal TextBlock WeatherPreviewSectionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewSectionTextBlock")!;
internal TextBlock WeatherSettingsSectionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherSettingsSectionTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherIconPackSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherIconPackSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherNoTlsSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherNoTlsSettingsExpander")!;
internal Button WeatherPreviewButton => WeatherSettingsPanel.FindControl<Button>("WeatherPreviewButton")!;
internal ComboBox WeatherLocationModeComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherLocationModeComboBox")!;
internal ComboBoxItem WeatherLocationModeCityItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCityItem")!;
internal ComboBoxItem WeatherLocationModeCoordinatesItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherLocationModeCoordinatesItem")!;
internal ListBoxItem WeatherLocationModeCityChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCityChipItem")!;
internal ListBoxItem WeatherLocationModeCoordinatesChipItem => WeatherSettingsPanel.FindControl<ListBoxItem>("WeatherLocationModeCoordinatesChipItem")!;
internal ListBox WeatherLocationModeChipListBox => WeatherSettingsPanel.FindControl<ListBox>("WeatherLocationModeChipListBox")!;
internal ToggleSwitch WeatherAutoRefreshToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherAutoRefreshToggleSwitch")!;
internal Button WeatherSearchButton => WeatherSettingsPanel.FindControl<Button>("WeatherSearchButton")!;
internal Button WeatherApplyCityButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCityButton")!;
internal Button WeatherApplyCoordinatesButton => WeatherSettingsPanel.FindControl<Button>("WeatherApplyCoordinatesButton")!;
internal TextBox WeatherExcludedAlertsTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherExcludedAlertsTextBox")!;
internal ComboBox WeatherIconPackComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherIconPackComboBox")!;
internal ToggleSwitch WeatherNoTlsToggleSwitch => WeatherSettingsPanel.FindControl<ToggleSwitch>("WeatherNoTlsToggleSwitch")!;
internal TextBox WeatherCitySearchTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherCitySearchTextBox")!;
internal ComboBox WeatherCityResultsComboBox => WeatherSettingsPanel.FindControl<ComboBox>("WeatherCityResultsComboBox")!;
internal TextBlock WeatherSearchStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherSearchStatusTextBlock")!;
internal TextBox WeatherLocationKeyTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationKeyTextBox")!;
internal TextBox WeatherLocationNameTextBox => WeatherSettingsPanel.FindControl<TextBox>("WeatherLocationNameTextBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLatitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLatitudeNumberBox")!;
internal FluentAvalonia.UI.Controls.NumberBox WeatherLongitudeNumberBox => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLongitudeNumberBox")!;
internal TextBlock WeatherCoordinateStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherCoordinateStatusTextBlock")!;
internal TextBlock WeatherPreviewResultTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewResultTextBlock")!;
internal Image WeatherPreviewIconImage => WeatherSettingsPanel.FindControl<Image>("WeatherPreviewIconImage")!;
internal FluentIcons.Avalonia.Fluent.SymbolIcon WeatherPreviewIconSymbol => WeatherSettingsPanel.FindControl<FluentIcons.Avalonia.Fluent.SymbolIcon>("WeatherPreviewIconSymbol")!;
internal TextBlock WeatherPreviewTemperatureTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewTemperatureTextBlock")!;
internal TextBlock WeatherPreviewUpdatedTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewUpdatedTextBlock")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherSearchProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherSearchProgressRing")!;
internal FluentAvalonia.UI.Controls.ProgressRing WeatherPreviewProgressRing => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherPreviewProgressRing")!;
internal ComboBoxItem WeatherIconPackFluentRegularItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentRegularItem")!;
internal ComboBoxItem WeatherIconPackFluentFilledItem => WeatherSettingsPanel.FindControl<ComboBoxItem>("WeatherIconPackFluentFilledItem")!;
internal TextBlock WeatherLocationSelectionTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationSelectionTitleTextBlock")!;
internal TextBlock WeatherLocationSelectionDescriptionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationSelectionDescriptionTextBlock")!;
internal TextBlock WeatherLocationValueTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationValueTextBlock")!;
internal TextBlock WeatherLocationStatusTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherLocationStatusTextBlock")!;
internal TextBlock WeatherAlertListTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherAlertListTitleTextBlock")!;
internal TextBlock WeatherAlertListDescriptionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherAlertListDescriptionTextBlock")!;
internal TextBlock WeatherFooterHintTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherFooterHintTextBlock")!;
private WallpaperSettingsPage WallpaperSettingsPageRoot => RequireSettingsPage(WallpaperSettingsPanel, nameof(WallpaperSettingsPanel));
private GridSettingsPage GridSettingsPageRoot => RequireSettingsPage(GridSettingsPanel, nameof(GridSettingsPanel));
private ColorSettingsPage ColorSettingsPageRoot => RequireSettingsPage(ColorSettingsPanel, nameof(ColorSettingsPanel));
private StatusBarSettingsPage StatusBarSettingsPageRoot => RequireSettingsPage(StatusBarSettingsPanel, nameof(StatusBarSettingsPanel));
private RegionSettingsPage RegionSettingsPageRoot => RequireSettingsPage(RegionSettingsPanel, nameof(RegionSettingsPanel));
private WeatherSettingsPage WeatherSettingsPageRoot => RequireSettingsPage(WeatherSettingsPanel, nameof(WeatherSettingsPanel));
private UpdateSettingsPage UpdateSettingsPageRoot => RequireSettingsPage(UpdateSettingsPanel, nameof(UpdateSettingsPanel));
private AboutSettingsPage AboutSettingsPageRoot => RequireSettingsPage(AboutSettingsPanel, nameof(AboutSettingsPanel));
private LauncherSettingsPage LauncherSettingsPageRoot => RequireSettingsPage(LauncherSettingsPanel, nameof(LauncherSettingsPanel));
// --- UpdateSettingsPage ---
internal TextBlock UpdatePanelTitleTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander")!;
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionLabelTextBlock")!;
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionValueTextBlock")!;
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionLabelTextBlock")!;
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionValueTextBlock")!;
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtLabelTextBlock")!;
internal TextBlock UpdatePublishedAtValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtValueTextBlock")!;
internal TextBlock UpdateChannelLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateChannelLabelTextBlock")!;
internal ListBoxItem UpdateChannelStableChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelStableChipItem")!;
internal ListBoxItem UpdateChannelPreviewChipItem => UpdateSettingsPanel.FindControl<ListBoxItem>("UpdateChannelPreviewChipItem")!;
internal ToggleSwitch AutoCheckUpdatesToggleSwitch => UpdateSettingsPanel.FindControl<ToggleSwitch>("AutoCheckUpdatesToggleSwitch")! ;
internal ListBox UpdateChannelChipListBox => UpdateSettingsPanel.FindControl<ListBox>("UpdateChannelChipListBox")!;
internal Button CheckForUpdatesButton => UpdateSettingsPanel.FindControl<Button>("CheckForUpdatesButton")!;
internal Button DownloadAndInstallUpdateButton => UpdateSettingsPanel.FindControl<Button>("DownloadAndInstallUpdateButton")!;
internal ProgressBar UpdateDownloadProgressBar => UpdateSettingsPanel.FindControl<ProgressBar>("UpdateDownloadProgressBar")!;
internal TextBlock UpdateDownloadProgressTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateDownloadProgressTextBlock")!;
internal TextBlock UpdateStatusTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateStatusTextBlock")!;
private T WallpaperControl<T>(string name) where T : Control => RequirePageControl<T>(WallpaperSettingsPageRoot, name);
private T GridControl<T>(string name) where T : Control => RequirePageControl<T>(GridSettingsPageRoot, name);
private T ColorControl<T>(string name) where T : Control => RequirePageControl<T>(ColorSettingsPageRoot, name);
private T StatusBarControl<T>(string name) where T : Control => RequirePageControl<T>(StatusBarSettingsPageRoot, name);
private T RegionControl<T>(string name) where T : Control => RequirePageControl<T>(RegionSettingsPageRoot, name);
private T WeatherControl<T>(string name) where T : Control => RequirePageControl<T>(WeatherSettingsPageRoot, name);
private T UpdateControl<T>(string name) where T : Control => RequirePageControl<T>(UpdateSettingsPageRoot, name);
private T AboutControl<T>(string name) where T : Control => RequirePageControl<T>(AboutSettingsPageRoot, name);
private T LauncherControl<T>(string name) where T : Control => RequirePageControl<T>(LauncherSettingsPageRoot, name);
// --- AboutSettingsPage ---
internal TextBlock AboutPanelTitleTextBlock => AboutSettingsPanel.FindControl<TextBlock>("AboutPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander")!;
internal FluentAvalonia.UI.Controls.SettingsExpander AboutRenderModeSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutRenderModeSettingsExpander")!;
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutSettingsPanel.FindControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch")!;
internal ComboBox AppRenderModeComboBox => AboutSettingsPanel.FindControl<ComboBox>("AppRenderModeComboBox")!;
internal TextBlock CurrentRenderBackendLabelTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendLabelTextBlock")!;
internal TextBlock CurrentRenderBackendValueTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendValueTextBlock")!;
internal TextBlock CurrentRenderBackendImplementationTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendImplementationTextBlock")!;
internal TextBlock VersionTextBlock => AboutSettingsPanel.FindControl<TextBlock>("VersionTextBlock")!;
internal TextBlock CodeNameTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CodeNameTextBlock")!;
internal TextBlock FontInfoTextBlock => AboutSettingsPanel.FindControl<TextBlock>("FontInfoTextBlock")!;
internal TextBlock WallpaperPanelTitleTextBlock => WallpaperControl<TextBlock>("WallpaperPanelTitleTextBlock");
internal TextBlock WallpaperPathTextBlock => WallpaperControl<TextBlock>("WallpaperPathTextBlock");
internal TextBlock WallpaperStatusTextBlock => WallpaperControl<TextBlock>("WallpaperStatusTextBlock");
internal ComboBox WallpaperPlacementComboBox => WallpaperControl<ComboBox>("WallpaperPlacementComboBox");
internal Border WallpaperPreviewHost => WallpaperControl<Border>("WallpaperPreviewHost");
internal Border WallpaperPreviewFrame => WallpaperControl<Border>("WallpaperPreviewFrame");
internal Border WallpaperPreviewViewport => WallpaperControl<Border>("WallpaperPreviewViewport");
internal Image WallpaperPreviewVideoImage => WallpaperControl<Image>("WallpaperPreviewVideoImage");
internal Grid WallpaperPreviewGrid => WallpaperControl<Grid>("WallpaperPreviewGrid");
internal Border WallpaperPreviewTopStatusBarHost => WallpaperControl<Border>("WallpaperPreviewTopStatusBarHost");
internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel");
internal ClockWidget WallpaperPreviewClockWidget => WallpaperControl<ClockWidget>("WallpaperPreviewClockWidget");
internal Border WallpaperPreviewBottomTaskbarContainer => WallpaperControl<Border>("WallpaperPreviewBottomTaskbarContainer");
internal Border WallpaperPreviewTaskbarFixedActionsHost => WallpaperControl<Border>("WallpaperPreviewTaskbarFixedActionsHost");
internal StackPanel WallpaperPreviewBackButtonVisual => WallpaperControl<StackPanel>("WallpaperPreviewBackButtonVisual");
internal TextBlock WallpaperPreviewBackButtonTextBlock => WallpaperControl<TextBlock>("WallpaperPreviewBackButtonTextBlock");
internal StackPanel WallpaperPreviewTaskbarDynamicActionsHost => WallpaperControl<StackPanel>("WallpaperPreviewTaskbarDynamicActionsHost");
internal Border WallpaperPreviewTaskbarSettingsActionHost => WallpaperControl<Border>("WallpaperPreviewTaskbarSettingsActionHost");
internal StackPanel WallpaperPreviewComponentLibraryVisual => WallpaperControl<StackPanel>("WallpaperPreviewComponentLibraryVisual");
internal TextBlock WallpaperPreviewComponentLibraryTextBlock => WallpaperControl<TextBlock>("WallpaperPreviewComponentLibraryTextBlock");
internal FluentIcons.Avalonia.SymbolIcon WallpaperPreviewSettingsButtonIcon => WallpaperControl<FluentIcons.Avalonia.SymbolIcon>("WallpaperPreviewSettingsButtonIcon");
internal Button PickWallpaperButton => WallpaperControl<Button>("PickWallpaperButton");
internal Button ClearWallpaperButton => WallpaperControl<Button>("ClearWallpaperButton");
internal FluentAvalonia.UI.Controls.SettingsExpander WallpaperPlacementSettingsExpander => WallpaperControl<FluentAvalonia.UI.Controls.SettingsExpander>("WallpaperPlacementSettingsExpander");
private Image? OptionalWallpaperPreviewVideoImage => TryGetOptionalPageControl<Image>(WallpaperSettingsPanel, "WallpaperPreviewVideoImage");
private Border? OptionalWallpaperPreviewViewport => TryGetOptionalPageControl<Border>(WallpaperSettingsPanel, "WallpaperPreviewViewport");
private bool IsWallpaperSettingsPageVisible => string.Equals(NormalizeSettingsPageTag(_selectedSettingsTabTag), "Appearance", StringComparison.OrdinalIgnoreCase);
// --- LauncherSettingsPage ---
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherSettingsPanelTitleTextBlock")!;
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander")!;
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock")!;
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock")!;
internal TextBlock GridPanelTitleTextBlock => GridControl<TextBlock>("GridPanelTitleTextBlock");
internal Border GridPreviewHost => GridControl<Border>("GridPreviewHost");
internal Border GridPreviewFrame => GridControl<Border>("GridPreviewFrame");
internal Border GridPreviewViewport => GridControl<Border>("GridPreviewViewport");
internal Canvas GridPreviewLinesCanvas => GridControl<Canvas>("GridPreviewLinesCanvas");
internal Grid GridPreviewGrid => GridControl<Grid>("GridPreviewGrid");
internal Border GridPreviewTopStatusBarHost => GridControl<Border>("GridPreviewTopStatusBarHost");
internal StackPanel GridPreviewTopStatusComponentsPanel => GridControl<StackPanel>("GridPreviewTopStatusComponentsPanel");
internal Border GridPreviewBottomTaskbarContainer => GridControl<Border>("GridPreviewBottomTaskbarContainer");
internal Border GridPreviewTaskbarFixedActionsHost => GridControl<Border>("GridPreviewTaskbarFixedActionsHost");
internal StackPanel GridPreviewBackButtonVisual => GridControl<StackPanel>("GridPreviewBackButtonVisual");
internal TextBlock GridPreviewBackButtonTextBlock => GridControl<TextBlock>("GridPreviewBackButtonTextBlock");
internal StackPanel GridPreviewTaskbarDynamicActionsHost => GridControl<StackPanel>("GridPreviewTaskbarDynamicActionsHost");
internal Border GridPreviewTaskbarSettingsActionHost => GridControl<Border>("GridPreviewTaskbarSettingsActionHost");
internal StackPanel GridPreviewComponentLibraryVisual => GridControl<StackPanel>("GridPreviewComponentLibraryVisual");
internal FluentIcons.Avalonia.FluentIcon GridPreviewComponentLibraryIcon => GridControl<FluentIcons.Avalonia.FluentIcon>("GridPreviewComponentLibraryIcon");
internal TextBlock GridPreviewComponentLibraryTextBlock => GridControl<TextBlock>("GridPreviewComponentLibraryTextBlock");
internal FluentIcons.Avalonia.SymbolIcon GridPreviewSettingsButtonIcon => GridControl<FluentIcons.Avalonia.SymbolIcon>("GridPreviewSettingsButtonIcon");
internal FluentAvalonia.UI.Controls.SettingsExpander GridRowsSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridRowsSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander GridSpacingSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridSpacingSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander GridEdgeInsetSettingsExpander => GridControl<FluentAvalonia.UI.Controls.SettingsExpander>("GridEdgeInsetSettingsExpander");
internal Slider GridSizeSlider => GridControl<Slider>("GridSizeSlider");
internal FluentAvalonia.UI.Controls.NumberBox GridSizeNumberBox => GridControl<FluentAvalonia.UI.Controls.NumberBox>("GridSizeNumberBox");
internal ComboBox GridSpacingPresetComboBox => GridControl<ComboBox>("GridSpacingPresetComboBox");
internal ComboBoxItem GridSpacingRelaxedComboBoxItem => GridControl<ComboBoxItem>("GridSpacingRelaxedComboBoxItem");
internal ComboBoxItem GridSpacingCompactComboBoxItem => GridControl<ComboBoxItem>("GridSpacingCompactComboBoxItem");
internal Slider GridEdgeInsetSlider => GridControl<Slider>("GridEdgeInsetSlider");
internal FluentAvalonia.UI.Controls.NumberBox GridEdgeInsetNumberBox => GridControl<FluentAvalonia.UI.Controls.NumberBox>("GridEdgeInsetNumberBox");
internal TextBlock GridEdgeInsetComputedPxTextBlock => GridControl<TextBlock>("GridEdgeInsetComputedPxTextBlock");
internal Button ApplyGridButton => GridControl<Button>("ApplyGridButton");
internal TextBlock GridInfoTextBlock => GridControl<TextBlock>("GridInfoTextBlock");
internal TextBlock ColorPanelTitleTextBlock => ColorControl<TextBlock>("ColorPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeModeSettingsExpander => ColorControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeModeSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander ThemeColorSettingsExpander => ColorControl<FluentAvalonia.UI.Controls.SettingsExpander>("ThemeColorSettingsExpander");
internal ToggleSwitch NightModeToggleSwitch => ColorControl<ToggleSwitch>("NightModeToggleSwitch");
internal TextBlock ThemeColorStatusTextBlock => ColorControl<TextBlock>("ThemeColorStatusTextBlock");
internal TextBlock RecommendedColorsLabelTextBlock => ColorControl<TextBlock>("RecommendedColorsLabelTextBlock");
internal TextBlock SystemMonetColorsLabelTextBlock => ColorControl<TextBlock>("SystemMonetColorsLabelTextBlock");
internal Button RecommendedColorButton1 => ColorControl<Button>("RecommendedColorButton1");
internal Button RecommendedColorButton2 => ColorControl<Button>("RecommendedColorButton2");
internal Button RecommendedColorButton3 => ColorControl<Button>("RecommendedColorButton3");
internal Button RecommendedColorButton4 => ColorControl<Button>("RecommendedColorButton4");
internal Button RecommendedColorButton5 => ColorControl<Button>("RecommendedColorButton5");
internal Button RecommendedColorButton6 => ColorControl<Button>("RecommendedColorButton6");
internal Border RecommendedColorSwatch1 => ColorControl<Border>("RecommendedColorSwatch1");
internal Border RecommendedColorSwatch2 => ColorControl<Border>("RecommendedColorSwatch2");
internal Border RecommendedColorSwatch3 => ColorControl<Border>("RecommendedColorSwatch3");
internal Border RecommendedColorSwatch4 => ColorControl<Border>("RecommendedColorSwatch4");
internal Border RecommendedColorSwatch5 => ColorControl<Border>("RecommendedColorSwatch5");
internal Border RecommendedColorSwatch6 => ColorControl<Border>("RecommendedColorSwatch6");
internal Button RefreshMonetColorsButton => ColorControl<Button>("RefreshMonetColorsButton");
internal Button MonetColorButton1 => ColorControl<Button>("MonetColorButton1");
internal Button MonetColorButton2 => ColorControl<Button>("MonetColorButton2");
internal Button MonetColorButton3 => ColorControl<Button>("MonetColorButton3");
internal Button MonetColorButton4 => ColorControl<Button>("MonetColorButton4");
internal Button MonetColorButton5 => ColorControl<Button>("MonetColorButton5");
internal Button MonetColorButton6 => ColorControl<Button>("MonetColorButton6");
internal Border MonetColorSwatch1 => ColorControl<Border>("MonetColorSwatch1");
internal Border MonetColorSwatch2 => ColorControl<Border>("MonetColorSwatch2");
internal Border MonetColorSwatch3 => ColorControl<Border>("MonetColorSwatch3");
internal Border MonetColorSwatch4 => ColorControl<Border>("MonetColorSwatch4");
internal Border MonetColorSwatch5 => ColorControl<Border>("MonetColorSwatch5");
internal Border MonetColorSwatch6 => ColorControl<Border>("MonetColorSwatch6");
internal TextBlock StatusBarPanelTitleTextBlock => StatusBarControl<TextBlock>("StatusBarPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarClockSettingsExpander => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarClockSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander StatusBarSpacingSettingsExpander => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpander>("StatusBarSpacingSettingsExpander");
internal ToggleSwitch StatusBarClockToggleSwitch => StatusBarControl<ToggleSwitch>("StatusBarClockToggleSwitch");
internal RadioButton ClockFormatHMSSRadio => StatusBarControl<RadioButton>("ClockFormatHMSSRadio");
internal RadioButton ClockFormatHMRadio => StatusBarControl<RadioButton>("ClockFormatHMRadio");
internal ComboBox StatusBarSpacingModeComboBox => StatusBarControl<ComboBox>("StatusBarSpacingModeComboBox");
internal ComboBoxItem StatusBarSpacingModeCompactItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeCompactItem");
internal ComboBoxItem StatusBarSpacingModeRelaxedItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeRelaxedItem");
internal ComboBoxItem StatusBarSpacingModeCustomItem => StatusBarControl<ComboBoxItem>("StatusBarSpacingModeCustomItem");
internal FluentAvalonia.UI.Controls.SettingsExpanderItem StatusBarSpacingCustomPanel => StatusBarControl<FluentAvalonia.UI.Controls.SettingsExpanderItem>("StatusBarSpacingCustomPanel");
internal Slider StatusBarSpacingSlider => StatusBarControl<Slider>("StatusBarSpacingSlider");
internal FluentAvalonia.UI.Controls.NumberBox StatusBarSpacingNumberBox => StatusBarControl<FluentAvalonia.UI.Controls.NumberBox>("StatusBarSpacingNumberBox");
internal TextBlock StatusBarSpacingComputedPxTextBlock => StatusBarControl<TextBlock>("StatusBarSpacingComputedPxTextBlock");
internal TextBlock RegionPanelTitleTextBlock => RegionControl<TextBlock>("RegionPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander LanguageSettingsExpander => RegionControl<FluentAvalonia.UI.Controls.SettingsExpander>("LanguageSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander TimeZoneSettingsExpander => RegionControl<FluentAvalonia.UI.Controls.SettingsExpander>("TimeZoneSettingsExpander");
internal ComboBox LanguageComboBox => RegionControl<ComboBox>("LanguageComboBox");
internal ComboBoxItem LanguageChineseItem => RegionControl<ComboBoxItem>("LanguageChineseItem");
internal ComboBoxItem LanguageEnglishItem => RegionControl<ComboBoxItem>("LanguageEnglishItem");
internal ComboBox TimeZoneComboBox => RegionControl<ComboBox>("TimeZoneComboBox");
internal TextBlock WeatherPanelTitleTextBlock => WeatherControl<TextBlock>("WeatherPanelTitleTextBlock");
internal TextBlock WeatherPreviewSectionTextBlock => WeatherControl<TextBlock>("WeatherPreviewSectionTextBlock");
internal TextBlock WeatherSettingsSectionTextBlock => WeatherControl<TextBlock>("WeatherSettingsSectionTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherIconPackSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherIconPackSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherNoTlsSettingsExpander => WeatherControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherNoTlsSettingsExpander");
internal Button WeatherPreviewButton => WeatherControl<Button>("WeatherPreviewButton");
internal ComboBox WeatherLocationModeComboBox => WeatherControl<ComboBox>("WeatherLocationModeComboBox");
internal ComboBoxItem WeatherLocationModeCityItem => WeatherControl<ComboBoxItem>("WeatherLocationModeCityItem");
internal ComboBoxItem WeatherLocationModeCoordinatesItem => WeatherControl<ComboBoxItem>("WeatherLocationModeCoordinatesItem");
internal ListBoxItem WeatherLocationModeCityChipItem => WeatherControl<ListBoxItem>("WeatherLocationModeCityChipItem");
internal ListBoxItem WeatherLocationModeCoordinatesChipItem => WeatherControl<ListBoxItem>("WeatherLocationModeCoordinatesChipItem");
internal ListBox WeatherLocationModeChipListBox => WeatherControl<ListBox>("WeatherLocationModeChipListBox");
internal ToggleSwitch WeatherAutoRefreshToggleSwitch => WeatherControl<ToggleSwitch>("WeatherAutoRefreshToggleSwitch");
internal Button WeatherSearchButton => WeatherControl<Button>("WeatherSearchButton");
internal Button WeatherApplyCityButton => WeatherControl<Button>("WeatherApplyCityButton");
internal Button WeatherApplyCoordinatesButton => WeatherControl<Button>("WeatherApplyCoordinatesButton");
internal TextBox WeatherExcludedAlertsTextBox => WeatherControl<TextBox>("WeatherExcludedAlertsTextBox");
internal ComboBox WeatherIconPackComboBox => WeatherControl<ComboBox>("WeatherIconPackComboBox");
internal ToggleSwitch WeatherNoTlsToggleSwitch => WeatherControl<ToggleSwitch>("WeatherNoTlsToggleSwitch");
internal TextBox WeatherCitySearchTextBox => WeatherControl<TextBox>("WeatherCitySearchTextBox");
internal ComboBox WeatherCityResultsComboBox => WeatherControl<ComboBox>("WeatherCityResultsComboBox");
internal TextBlock WeatherSearchStatusTextBlock => WeatherControl<TextBlock>("WeatherSearchStatusTextBlock");
internal TextBox WeatherLocationKeyTextBox => WeatherControl<TextBox>("WeatherLocationKeyTextBox");
internal TextBox WeatherLocationNameTextBox => WeatherControl<TextBox>("WeatherLocationNameTextBox");
internal FluentAvalonia.UI.Controls.NumberBox WeatherLatitudeNumberBox => WeatherControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLatitudeNumberBox");
internal FluentAvalonia.UI.Controls.NumberBox WeatherLongitudeNumberBox => WeatherControl<FluentAvalonia.UI.Controls.NumberBox>("WeatherLongitudeNumberBox");
internal TextBlock WeatherCoordinateStatusTextBlock => WeatherControl<TextBlock>("WeatherCoordinateStatusTextBlock");
internal TextBlock WeatherPreviewResultTextBlock => WeatherControl<TextBlock>("WeatherPreviewResultTextBlock");
internal Image WeatherPreviewIconImage => WeatherControl<Image>("WeatherPreviewIconImage");
internal FluentIcons.Avalonia.Fluent.SymbolIcon WeatherPreviewIconSymbol => WeatherControl<FluentIcons.Avalonia.Fluent.SymbolIcon>("WeatherPreviewIconSymbol");
internal TextBlock WeatherPreviewTemperatureTextBlock => WeatherControl<TextBlock>("WeatherPreviewTemperatureTextBlock");
internal TextBlock WeatherPreviewUpdatedTextBlock => WeatherControl<TextBlock>("WeatherPreviewUpdatedTextBlock");
internal FluentAvalonia.UI.Controls.ProgressRing WeatherSearchProgressRing => WeatherControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherSearchProgressRing");
internal FluentAvalonia.UI.Controls.ProgressRing WeatherPreviewProgressRing => WeatherControl<FluentAvalonia.UI.Controls.ProgressRing>("WeatherPreviewProgressRing");
internal ComboBoxItem WeatherIconPackFluentRegularItem => WeatherControl<ComboBoxItem>("WeatherIconPackFluentRegularItem");
internal ComboBoxItem WeatherIconPackFluentFilledItem => WeatherControl<ComboBoxItem>("WeatherIconPackFluentFilledItem");
internal TextBlock WeatherLocationSelectionTitleTextBlock => WeatherControl<TextBlock>("WeatherLocationSelectionTitleTextBlock");
internal TextBlock WeatherLocationSelectionDescriptionTextBlock => WeatherControl<TextBlock>("WeatherLocationSelectionDescriptionTextBlock");
internal TextBlock WeatherLocationValueTextBlock => WeatherControl<TextBlock>("WeatherLocationValueTextBlock");
internal TextBlock WeatherLocationStatusTextBlock => WeatherControl<TextBlock>("WeatherLocationStatusTextBlock");
internal TextBlock WeatherAlertListTitleTextBlock => WeatherControl<TextBlock>("WeatherAlertListTitleTextBlock");
internal TextBlock WeatherAlertListDescriptionTextBlock => WeatherControl<TextBlock>("WeatherAlertListDescriptionTextBlock");
internal TextBlock WeatherFooterHintTextBlock => WeatherControl<TextBlock>("WeatherFooterHintTextBlock");
internal TextBlock UpdatePanelTitleTextBlock => UpdateControl<TextBlock>("UpdatePanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander");
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateControl<TextBlock>("UpdateCurrentVersionLabelTextBlock");
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateControl<TextBlock>("UpdateCurrentVersionValueTextBlock");
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateControl<TextBlock>("UpdateLatestVersionLabelTextBlock");
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateControl<TextBlock>("UpdateLatestVersionValueTextBlock");
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateControl<TextBlock>("UpdatePublishedAtLabelTextBlock");
internal TextBlock UpdatePublishedAtValueTextBlock => UpdateControl<TextBlock>("UpdatePublishedAtValueTextBlock");
internal TextBlock UpdateChannelLabelTextBlock => UpdateControl<TextBlock>("UpdateChannelLabelTextBlock");
internal ListBoxItem UpdateChannelStableChipItem => UpdateControl<ListBoxItem>("UpdateChannelStableChipItem");
internal ListBoxItem UpdateChannelPreviewChipItem => UpdateControl<ListBoxItem>("UpdateChannelPreviewChipItem");
internal ToggleSwitch AutoCheckUpdatesToggleSwitch => UpdateControl<ToggleSwitch>("AutoCheckUpdatesToggleSwitch");
internal ListBox UpdateChannelChipListBox => UpdateControl<ListBox>("UpdateChannelChipListBox");
internal Button CheckForUpdatesButton => UpdateControl<Button>("CheckForUpdatesButton");
internal Button DownloadAndInstallUpdateButton => UpdateControl<Button>("DownloadAndInstallUpdateButton");
internal ProgressBar UpdateDownloadProgressBar => UpdateControl<ProgressBar>("UpdateDownloadProgressBar");
internal TextBlock UpdateDownloadProgressTextBlock => UpdateControl<TextBlock>("UpdateDownloadProgressTextBlock");
internal TextBlock UpdateStatusTextBlock => UpdateControl<TextBlock>("UpdateStatusTextBlock");
internal TextBlock AboutPanelTitleTextBlock => AboutControl<TextBlock>("AboutPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutRenderModeSettingsExpander => AboutControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutRenderModeSettingsExpander");
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch");
internal ComboBox AppRenderModeComboBox => AboutControl<ComboBox>("AppRenderModeComboBox");
internal TextBlock CurrentRenderBackendLabelTextBlock => AboutControl<TextBlock>("CurrentRenderBackendLabelTextBlock");
internal TextBlock CurrentRenderBackendValueTextBlock => AboutControl<TextBlock>("CurrentRenderBackendValueTextBlock");
internal TextBlock CurrentRenderBackendImplementationTextBlock => AboutControl<TextBlock>("CurrentRenderBackendImplementationTextBlock");
internal TextBlock VersionTextBlock => AboutControl<TextBlock>("VersionTextBlock");
internal TextBlock CodeNameTextBlock => AboutControl<TextBlock>("CodeNameTextBlock");
internal TextBlock FontInfoTextBlock => AboutControl<TextBlock>("FontInfoTextBlock");
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherControl<TextBlock>("LauncherSettingsPanelTitleTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander");
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock");
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock");
}

View File

@@ -8,149 +8,332 @@ using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views;
using FluentIconVariant = FluentIcons.Common.IconVariant;
using FluentSymbol = FluentIcons.Common.Symbol;
using FluentSymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
public partial class SettingsWindow
{
protected override void OnClosed(EventArgs e)
private readonly Dictionary<string, Control> _builtInSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
internal void Open(string? pageTag = null)
{
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = null;
StopVideoWallpaper();
_previewVideoWallpaperPlayer?.Dispose();
_previewVideoWallpaperPlayer = null;
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
_previewVideoFrameRefreshTimer?.Stop();
_previewVideoFrameRefreshTimer = null;
_libVlc?.Dispose();
_libVlc = null;
_releaseUpdateService.Dispose();
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
foreach (var icon in _launcherIconCache.Values)
if (!string.IsNullOrWhiteSpace(pageTag))
{
icon.Dispose();
_selectedSettingsTabTag = NormalizeSettingsPageTag(pageTag);
if (_independentModuleInitializationCompleted)
{
SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
}
}
_launcherIconCache.Clear();
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
base.OnClosed(e);
if (!IsVisible)
{
Show();
}
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
}
internal void PrepareForForceClose()
{
_allowIndependentSettingsModuleRealClose = true;
}
protected override void OnClosed(EventArgs e)
{
AppLogger.Info(
"IndependentSettingsModule",
$"PreviewCleanupStarted; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; CloseRequested={_isIndependentSettingsModuleClosing}.");
try
{
_persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = null;
StopVideoWallpaper();
_previewVideoWallpaperPlayer?.Dispose();
_previewVideoWallpaperPlayer = null;
_previewVideoWallpaperMedia?.Dispose();
_previewVideoWallpaperMedia = null;
_previewVideoFrameRefreshTimer?.Stop();
_previewVideoFrameRefreshTimer = null;
_libVlc?.Dispose();
_libVlc = null;
_releaseUpdateService.Dispose();
_wallpaperBitmap?.Dispose();
_wallpaperBitmap = null;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
foreach (var icon in _launcherIconCache.Values)
{
icon.Dispose();
}
_launcherIconCache.Clear();
AppLogger.Info(
"IndependentSettingsModule",
$"PreviewCleanupCompleted; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; CloseRequested={_isIndependentSettingsModuleClosing}.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn(
"IndependentSettingsModule",
$"PreviewCleanupFailed; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; Downgraded=True; CloseRequested={_isIndependentSettingsModuleClosing}.",
ex);
}
finally
{
PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
Closing -= OnIndependentSettingsModuleClosing;
base.OnClosed(e);
AppLogger.Info("IndependentSettingsModule", $"WindowClosed; CloseRequested={_isIndependentSettingsModuleClosing}.");
_isIndependentSettingsModuleClosing = false;
_allowIndependentSettingsModuleRealClose = false;
}
}
private void InitializeSettingsNavigation()
{
_settingsPageDefinitions.Clear();
_settingsNavItems.Clear();
_pluginSettingsNavItems.Clear();
SettingsPrimaryNavHost.Children.Clear();
SettingsSecondaryNavHost.Children.Clear();
SettingsPluginNavHost.Children.Clear();
SettingsPluginNavSection.IsVisible = false;
AddSettingsNavItem(SettingsPrimaryNavHost, "Wallpaper", Symbol.Wallpaper, "Wallpaper");
AddSettingsNavItem(SettingsPrimaryNavHost, "Grid", Symbol.Grid, "Grid");
AddSettingsNavItem(SettingsPrimaryNavHost, "Color", Symbol.Color, "Color");
AddSettingsNavItem(SettingsPrimaryNavHost, "StatusBar", Symbol.Status, "Status Bar");
AddSettingsNavItem(SettingsPrimaryNavHost, "Weather", Symbol.WeatherSunny, "Weather");
AddSettingsNavItem(SettingsSecondaryNavHost, "Region", Symbol.Globe, "Region");
AddSettingsNavItem(SettingsSecondaryNavHost, "Launcher", Symbol.Apps, "App Launcher");
AddSettingsNavItem(SettingsSecondaryNavHost, "Update", Symbol.ArrowSync, "Update");
AddSettingsNavItem(SettingsSecondaryNavHost, "About", Symbol.Info, "About");
AddSettingsNavItem(SettingsSecondaryNavHost, "Plugins", Symbol.PuzzlePiece, "Plugins");
AddSettingsNavItem(SettingsSecondaryNavHost, "PluginMarket", Symbol.PuzzlePiece, "Plugin Market");
InitializePluginSettingsNavigation();
RegisterBuiltInSettingsPageDefinitions();
RegisterPluginSettingsDefinitions();
RebuildSettingsNavigationMenu();
}
private void OnSettingsNavItemClick(object? sender, RoutedEventArgs e)
private void RegisterBuiltInSettingsPageDefinitions()
{
if (sender is not Button button || button.Tag is not string tag)
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"General",
L("settings.nav.general", "General"),
L("settings.page_desc.general", "Manage language, launcher, and weather behavior from the independent settings module."),
FluentSymbol.Settings,
IndependentSettingsPageCategory.Internal,
0));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Appearance",
L("settings.nav.appearance", "Appearance"),
L("settings.page_desc.appearance", "Personalize wallpaper, desktop grid, and accent colors in one place."),
FluentSymbol.PaintBrush,
IndependentSettingsPageCategory.Internal,
10));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Components",
L("settings.nav.components", "Components"),
L("settings.page_desc.components", "Review available desktop components and configure the status bar area."),
FluentSymbol.Apps,
IndependentSettingsPageCategory.Internal,
20));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Update",
L("settings.nav.update", "Update"),
L("settings.page_desc.update", "Check for updates and control the update channel."),
FluentSymbol.ArrowSync,
IndependentSettingsPageCategory.Internal,
30));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"Plugins",
L("settings.nav.plugins", "Plugins"),
L("settings.page_desc.plugins", "Review installed plugins, runtime state, and local package installation."),
FluentSymbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
100));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"PluginMarket",
L("settings.nav.plugin_market", "Plugin Market"),
L("settings.page_desc.pluginmarket", "Browse the official plugin market and stage installs safely."),
FluentSymbol.ShoppingBag,
IndependentSettingsPageCategory.External,
110));
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
"About",
L("settings.nav.about", "About"),
L("settings.page_desc.about", "See version information, rendering backend, and startup behavior."),
FluentSymbol.Info,
IndependentSettingsPageCategory.About,
200));
}
private void RegisterSettingsPageDefinition(IndependentSettingsPageDefinition definition)
{
_settingsPageDefinitions[definition.Tag] = definition;
}
private void RebuildSettingsNavigationMenu()
{
if (SettingsNavView is null)
{
return;
}
SelectSettingsTab(tag, persistSelection: true);
}
var selectedTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
SettingsNavView.MenuItems.Clear();
_settingsNavItems.Clear();
_pluginSettingsNavItems.Clear();
private Button AddSettingsNavItem(Panel host, string tag, Symbol symbol, string title)
{
var button = CreateSettingsNavItem(tag, symbol, title);
host.Children.Add(button);
_settingsNavItems[tag] = button;
return button;
}
private Button CreateSettingsNavItem(string tag, Symbol symbol, string title)
{
var icon = new SymbolIcon
IndependentSettingsPageCategory? lastCategory = null;
foreach (var definition in _settingsPageDefinitions.Values
.OrderBy(definition => GetSettingsPageCategoryOrder(definition.Category))
.ThenBy(definition => definition.SortOrder)
.ThenBy(definition => definition.Title, StringComparer.CurrentCulture))
{
Symbol = symbol,
IconVariant = IconVariant.Regular
};
icon.Classes.Add("settings-nav-icon");
if (lastCategory is not null && lastCategory != definition.Category)
{
SettingsNavView.MenuItems.Add(new NavigationViewItemSeparator());
}
var iconShell = new Border
{
Child = icon,
Classes = { "settings-sidebar-icon-shell" }
};
var navItem = CreateSettingsNavItem(definition);
SettingsNavView.MenuItems.Add(navItem);
_settingsNavItems[definition.Tag] = navItem;
if (definition.Category == IndependentSettingsPageCategory.External)
{
_pluginSettingsNavItems[definition.Tag] = navItem;
}
var label = new TextBlock
{
Text = title,
Classes = { "settings-nav-label" }
};
var contentGrid = new Grid
{
ColumnDefinitions = new ColumnDefinitions("Auto,*"),
ColumnSpacing = 12
};
contentGrid.Children.Add(iconShell);
contentGrid.Children.Add(label);
Grid.SetColumn(label, 1);
var button = new Button
{
Tag = tag,
Content = contentGrid,
Classes = { "settings-sidebar-item" }
};
button.Click += OnSettingsNavItemClick;
return button;
}
private IEnumerable<Button> EnumerateSettingsNavItems()
{
foreach (var button in SettingsPrimaryNavHost.Children.OfType<Button>())
{
yield return button;
lastCategory = definition.Category;
}
foreach (var button in SettingsSecondaryNavHost.Children.OfType<Button>())
if (_settingsNavItems.TryGetValue(selectedTag, out var selectedItem))
{
yield return button;
SettingsNavView.SelectedItem = selectedItem;
return;
}
foreach (var button in SettingsPluginNavHost.Children.OfType<Button>())
if (SettingsNavView.MenuItems.OfType<NavigationViewItem>().FirstOrDefault() is { } firstItem)
{
yield return button;
_selectedSettingsTabTag = firstItem.Tag?.ToString() ?? "General";
SettingsNavView.SelectedItem = firstItem;
}
}
private Button? GetSettingsNavItem(string tag)
private NavigationViewItem CreateSettingsNavItem(IndependentSettingsPageDefinition definition)
{
var item = new NavigationViewItem
{
Content = definition.Title,
Tag = definition.Tag,
IconSource = new FluentSymbolIconSource
{
Symbol = definition.Icon,
IconVariant = FluentIconVariant.Regular
}
};
if (!string.IsNullOrWhiteSpace(definition.ToolTip))
{
ToolTip.SetTip(item, definition.ToolTip);
}
return item;
}
private static int GetSettingsPageCategoryOrder(IndependentSettingsPageCategory category)
{
return category switch
{
IndependentSettingsPageCategory.Internal => 0,
IndependentSettingsPageCategory.External => 1,
IndependentSettingsPageCategory.About => 2,
IndependentSettingsPageCategory.Debug => 3,
_ => int.MaxValue
};
}
private void InitializeSettingsPageHosts()
{
_builtInSettingsPageHosts.Clear();
GeneralSettingsHubPanel = new GeneralSettingsPage();
AppearanceSettingsHubPanel = new AppearanceSettingsPage();
ComponentsSettingsHubPanel = new ComponentsSettingsPage();
WallpaperSettingsPanel = new WallpaperSettingsPage();
GridSettingsPanel = new GridSettingsPage();
ColorSettingsPanel = new ColorSettingsPage();
StatusBarSettingsPanel = new StatusBarSettingsPage();
WeatherSettingsPanel = new WeatherSettingsPage();
RegionSettingsPanel = new RegionSettingsPage();
UpdateSettingsPanel = new UpdateSettingsPage();
LauncherSettingsPanel = new LauncherSettingsPage();
AboutSettingsPanel = new AboutSettingsPage();
PluginSettingsPanel = new PluginSettingsPage();
PluginMarketSettingsPanel = new PluginMarketSettingsPage();
GeneralSettingsHubPanel.RegionContentHost.Content = RegionSettingsPanel;
GeneralSettingsHubPanel.LauncherContentHost.Content = LauncherSettingsPanel;
GeneralSettingsHubPanel.WeatherContentHost.Content = WeatherSettingsPanel;
AppearanceSettingsHubPanel.WallpaperContentHost.Content = WallpaperSettingsPanel;
AppearanceSettingsHubPanel.GridContentHost.Content = GridSettingsPanel;
AppearanceSettingsHubPanel.ColorContentHost.Content = ColorSettingsPanel;
ComponentsSettingsHubPanel.StatusBarContentHost.Content = StatusBarSettingsPanel;
RegisterBuiltInSettingsPage("General", GeneralSettingsHubPanel);
RegisterBuiltInSettingsPage("Appearance", AppearanceSettingsHubPanel);
RegisterBuiltInSettingsPage("Components", ComponentsSettingsHubPanel);
RegisterBuiltInSettingsPage("Update", UpdateSettingsPanel);
RegisterBuiltInSettingsPage("About", AboutSettingsPanel);
RegisterBuiltInSettingsPage("Plugins", PluginSettingsPanel);
RegisterBuiltInSettingsPage("PluginMarket", PluginMarketSettingsPanel);
}
private void RegisterBuiltInSettingsPage(string tag, Control? page)
{
if (page is not null)
{
_builtInSettingsPageHosts[tag] = page;
}
}
private Control? ResolveSettingsPageHost(string? tag)
{
if (string.IsNullOrWhiteSpace(tag))
{
return null;
}
if (_builtInSettingsPageHosts.TryGetValue(tag, out var builtIn))
{
return builtIn;
}
return _pluginSettingsPageHosts.GetValueOrDefault(tag);
}
private void OnSettingsNavSelectionChanged(object? sender, NavigationViewSelectionChangedEventArgs e)
{
if (e.SelectedItem is NavigationViewItem selectedItem &&
selectedItem.Tag is not null)
{
_selectedSettingsTabTag = NormalizeSettingsPageTag(selectedItem.Tag.ToString());
}
AppLogger.Info("IndependentSettingsModule", $"NavigationChanged; Tag='{_selectedSettingsTabTag}'.");
UpdateSettingsTabContent();
SchedulePersistSettings(0);
}
private NavigationViewItem? GetSettingsNavItem(string tag)
{
if (_settingsNavItems.TryGetValue(tag, out var builtIn))
{
@@ -160,51 +343,20 @@ public partial class SettingsWindow
return _pluginSettingsNavItems.GetValueOrDefault(tag);
}
private static void SetSettingsNavItemLabel(Button? button, string text)
{
if (button?.Content is Grid grid)
{
var label = grid.Children
.OfType<TextBlock>()
.FirstOrDefault(textBlock => textBlock.Classes.Contains("settings-nav-label"));
if (label is not null)
{
label.Text = text;
}
}
}
private void SelectSettingsTab(string? tag, bool persistSelection)
{
if (string.IsNullOrWhiteSpace(tag))
if (string.IsNullOrWhiteSpace(tag) || SettingsNavView is null)
{
return;
}
var selectedButton = GetSettingsNavItem(tag);
if (selectedButton is null)
if (GetSettingsNavItem(tag) is not { } selectedItem)
{
return;
}
_selectedSettingsTabTag = tag;
foreach (var button in EnumerateSettingsNavItems())
{
var isSelected = ReferenceEquals(button, selectedButton);
if (isSelected)
{
if (!button.Classes.Contains("nav-selected"))
{
button.Classes.Add("nav-selected");
}
}
else
{
button.Classes.Remove("nav-selected");
}
}
SettingsNavView.SelectedItem = selectedItem;
UpdateSettingsTabContent();
if (persistSelection)
@@ -220,22 +372,31 @@ public partial class SettingsWindow
private void UpdateSettingsTabContent()
{
if (SettingsNavView is null || SettingsPageFrame is null)
{
return;
}
var tag = GetSelectedSettingsTabTag();
UpdateCurrentSettingsPageHeader(tag);
if (ResolveSettingsPageHost(tag) is { } pageHost)
{
if (!ReferenceEquals(SettingsPageFrame.Content, pageHost))
{
SettingsPageFrame.Content = pageHost;
}
}
else
{
AppLogger.Warn("IndependentSettingsModule", $"PageHostMissing; Tag='{tag}'.");
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
$"No settings page host is registered for '{tag}'.",
InfoBarSeverity.Warning);
return;
}
WallpaperSettingsPanel.IsVisible = tag == "Wallpaper";
GridSettingsPanel.IsVisible = tag == "Grid";
ColorSettingsPanel.IsVisible = tag == "Color";
StatusBarSettingsPanel.IsVisible = tag == "StatusBar";
WeatherSettingsPanel.IsVisible = tag == "Weather";
RegionSettingsPanel.IsVisible = tag == "Region";
UpdateSettingsPanel.IsVisible = tag == "Update";
AboutSettingsPanel.IsVisible = tag == "About";
LauncherSettingsPanel.IsVisible = tag == "Launcher";
PluginSettingsPanel.IsVisible = tag == "Plugins";
PluginMarketSettingsPanel.IsVisible = tag == "PluginMarket";
UpdatePluginSettingsPageVisibility(tag);
if (tag == "Launcher")
if (tag == "General")
{
RenderLauncherHiddenItemsList();
}
@@ -250,11 +411,17 @@ public partial class SettingsWindow
PluginMarketSettingsPanel.RefreshFromRuntime();
}
if (tag == "Grid")
if (tag == "Appearance")
{
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
}
if (tag == "Components")
{
UpdateComponentsSettingsSummary();
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
SyncVideoWallpaperPreviewPlayback();
}
@@ -266,6 +433,7 @@ public partial class SettingsWindow
return;
}
AppLogger.Info("IndependentSettingsModule", $"PersistCompleted; Tag='{_selectedSettingsTabTag}'.");
_appSettingsService.Save(BuildAppSettingsSnapshot());
_launcherSettingsService.Save(BuildLauncherSettingsSnapshot());
}
@@ -281,7 +449,7 @@ public partial class SettingsWindow
snapshot.WallpaperPath = _wallpaperPath;
snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement());
snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex());
snapshot.SettingsTabTag = GetSelectedSettingsTabTag();
snapshot.SettingsTabTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
snapshot.LanguageCode = _languageCode;
snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id;
snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode);
@@ -326,6 +494,7 @@ public partial class SettingsWindow
}
_persistSettingsDebounceTimer?.Dispose();
AppLogger.Info("IndependentSettingsModule", $"PersistScheduled; DelayMs={Math.Max(0, delayMs)}; Tag='{_selectedSettingsTabTag}'.");
_persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() =>
{
_persistSettingsDebounceTimer = null;
@@ -347,6 +516,22 @@ public partial class SettingsWindow
: "Relaxed";
}
private static string NormalizeSettingsPageTag(string? tag)
{
return tag switch
{
null or "" => "General",
_ when string.Equals(tag, "Wallpaper", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "Grid", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "Color", StringComparison.OrdinalIgnoreCase) => "Appearance",
_ when string.Equals(tag, "StatusBar", StringComparison.OrdinalIgnoreCase) => "Components",
_ when string.Equals(tag, "Region", StringComparison.OrdinalIgnoreCase) => "General",
_ when string.Equals(tag, "Weather", StringComparison.OrdinalIgnoreCase) => "General",
_ when string.Equals(tag, "Launcher", StringComparison.OrdinalIgnoreCase) => "General",
_ => tag
};
}
private static string NormalizeStatusBarSpacingMode(string? value)
{
return value switch
@@ -590,32 +775,32 @@ public partial class SettingsWindow
private void InitializeSettingsIcons()
{
const IconVariant variant = IconVariant.Regular;
const FluentIconVariant variant = FluentIconVariant.Regular;
WallpaperPlacementSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Wallpaper, IconVariant = variant };
ThemeColorSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant };
StatusBarClockSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Clock, IconVariant = variant };
StatusBarSpacingSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.TextLineSpacing, IconVariant = variant };
WeatherLocationSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant };
WeatherPreviewSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant };
WeatherAlertFilterSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Info, IconVariant = variant };
WeatherIconPackSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant };
WeatherNoTlsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Globe, IconVariant = variant };
LanguageSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Translate, IconVariant = variant };
TimeZoneSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.GlobeClock, IconVariant = variant };
UpdateOptionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowClockwiseDashesSettings, IconVariant = variant };
UpdateActionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowDownload, IconVariant = variant };
AboutStartupSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Play, IconVariant = variant };
PluginSystemSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.PuzzlePiece, IconVariant = variant };
WallpaperPlacementSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Wallpaper, IconVariant = variant };
ThemeColorSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
StatusBarClockSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Clock, IconVariant = variant };
StatusBarSpacingSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.TextLineSpacing, IconVariant = variant };
WeatherLocationSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherPreviewSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherAlertFilterSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Info, IconVariant = variant };
WeatherIconPackSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
WeatherNoTlsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Globe, IconVariant = variant };
LanguageSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Translate, IconVariant = variant };
TimeZoneSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.GlobeClock, IconVariant = variant };
UpdateOptionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowClockwiseDashesSettings, IconVariant = variant };
UpdateActionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowDownload, IconVariant = variant };
AboutStartupSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Play, IconVariant = variant };
PluginSystemSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.PuzzlePiece, IconVariant = variant };
UpdateThemeModeIcon();
}
private void UpdateThemeModeIcon()
{
ThemeModeSettingsExpander.IconSource = new SymbolIconSource
ThemeModeSettingsExpander.IconSource = new FluentSymbolIconSource
{
Symbol = _isNightMode ? Symbol.WeatherMoon : Symbol.WeatherSunny,
IconVariant = IconVariant.Regular
Symbol = _isNightMode ? FluentSymbol.WeatherMoon : FluentSymbol.WeatherSunny,
IconVariant = FluentIconVariant.Regular
};
}

View File

@@ -48,33 +48,50 @@ public partial class SettingsWindow
private void ApplyLocalization()
{
Title = L("settings.shell.title", "Application Settings");
WindowTitleTextBlock.Text = L("settings.shell.title", "Application Settings");
WindowSubtitleTextBlock.Text = L("settings.shell.subtitle", "LanMountainDesktop standalone preferences");
Title = L("settings.shell.title", "Settings");
WindowTitleTextBlock.Text = L("settings.shell.title", "Settings");
WindowSubtitleTextBlock.Text = L("settings.shell.subtitle", "LanMountainDesktop independent settings module");
WindowVersionBadgeTextBlock.Text = GetAppVersionText();
WindowCodeNameBadgeTextBlock.Text = AppCodeName;
SettingsSidebarTitleTextBlock.Text = L("settings.nav_header", "Settings");
SettingsSidebarHintTextBlock.Text = L(
"settings.shell.sidebar_hint",
"Choose a category to adjust application behavior and desktop appearance.");
SettingsPrimaryGroupTextBlock.Text = L("settings.nav.group_desktop", "Desktop");
SettingsSecondaryGroupTextBlock.Text = L("settings.nav.group_system", "System");
SettingsPluginGroupTextBlock.Text = L("settings.nav.group_extensions", "Extensions");
"Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm.");
SettingsSidebarFooterTextBlock.Text = L(
"settings.shell.footer_hint",
"Tray-opened settings are managed in this standalone window.");
"Tray-opened settings are managed in this independent settings module.");
InitializeSettingsNavigation();
SetSettingsNavItemLabel(GetSettingsNavItem("Wallpaper"), L("settings.nav.wallpaper", "Wallpaper"));
SetSettingsNavItemLabel(GetSettingsNavItem("Grid"), L("settings.nav.grid", "Grid"));
SetSettingsNavItemLabel(GetSettingsNavItem("Color"), L("settings.nav.color", "Color"));
SetSettingsNavItemLabel(GetSettingsNavItem("StatusBar"), L("settings.nav.status_bar", "Status Bar"));
SetSettingsNavItemLabel(GetSettingsNavItem("Weather"), L("settings.nav.weather", "Weather"));
SetSettingsNavItemLabel(GetSettingsNavItem("Region"), L("settings.nav.region", "Region"));
SetSettingsNavItemLabel(GetSettingsNavItem("Update"), L("settings.nav.update", "Update"));
SetSettingsNavItemLabel(GetSettingsNavItem("About"), L("settings.nav.about", "About"));
SetSettingsNavItemLabel(GetSettingsNavItem("Launcher"), L("settings.nav.launcher", "App Launcher"));
SetSettingsNavItemLabel(GetSettingsNavItem("Plugins"), L("settings.nav.plugins", "Plugins"));
SetSettingsNavItemLabel(GetSettingsNavItem("PluginMarket"), L("settings.nav.plugin_market", "Plugin Market"));
if (GeneralSettingsHubPanel is not null)
{
GeneralSettingsHubPanel.GeneralPageSubtitleTextBlock.Text = L("settings.page_desc.general", "Manage language, launcher, and weather behavior from the independent settings module.");
GeneralSettingsHubPanel.GeneralRegionSectionTitleTextBlock.Text = L("settings.nav.region", "Region");
GeneralSettingsHubPanel.GeneralRegionSectionHintTextBlock.Text = L("settings.general.region_hint", "Language and time zone settings affect the entire desktop shell.");
GeneralSettingsHubPanel.GeneralLauncherSectionTitleTextBlock.Text = L("settings.nav.launcher", "App Launcher");
GeneralSettingsHubPanel.GeneralLauncherSectionHintTextBlock.Text = L("settings.general.launcher_hint", "Restore hidden entries and adjust how the app launcher behaves.");
GeneralSettingsHubPanel.GeneralWeatherSectionTitleTextBlock.Text = L("settings.nav.weather", "Weather");
GeneralSettingsHubPanel.GeneralWeatherSectionHintTextBlock.Text = L("settings.general.weather_hint", "Configure shared weather source, location, and icon style for weather widgets.");
}
if (AppearanceSettingsHubPanel is not null)
{
AppearanceSettingsHubPanel.AppearancePageSubtitleTextBlock.Text = L("settings.page_desc.appearance", "Personalize wallpaper, desktop grid, and accent colors in one place.");
AppearanceSettingsHubPanel.AppearanceWallpaperSectionTitleTextBlock.Text = L("settings.nav.wallpaper", "Wallpaper");
AppearanceSettingsHubPanel.AppearanceWallpaperSectionHintTextBlock.Text = L("settings.appearance.wallpaper_hint", "Use lightweight thumbnails and asset controls instead of heavy live preview.");
AppearanceSettingsHubPanel.AppearanceGridSectionTitleTextBlock.Text = L("settings.nav.grid", "Grid");
AppearanceSettingsHubPanel.AppearanceGridSectionHintTextBlock.Text = L("settings.appearance.grid_hint", "Tune grid density, spacing, and safe edge inset for the desktop canvas.");
AppearanceSettingsHubPanel.AppearanceColorSectionTitleTextBlock.Text = L("settings.nav.color", "Color");
AppearanceSettingsHubPanel.AppearanceColorSectionHintTextBlock.Text = L("settings.appearance.color_hint", "Choose theme mode and accent colors with Fluent-consistent swatches.");
}
if (ComponentsSettingsHubPanel is not null)
{
ComponentsSettingsHubPanel.ComponentsPageSubtitleTextBlock.Text = L("settings.page_desc.components", "Review available desktop components and configure the status bar area.");
ComponentsSettingsHubPanel.ComponentsSummarySectionTitleTextBlock.Text = L("settings.components.library_title", "Component Library");
ComponentsSettingsHubPanel.ComponentsSummarySectionHintTextBlock.Text = L("settings.components.library_hint", "Built-in and plugin-contributed components available to the desktop editor.");
ComponentsSettingsHubPanel.ComponentsStatusBarSectionTitleTextBlock.Text = L("settings.nav.status_bar", "Status Bar");
ComponentsSettingsHubPanel.ComponentsStatusBarSectionHintTextBlock.Text = L("settings.components.status_bar_hint", "Clock and status-bar component spacing are managed here.");
}
WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Personalize your wallpaper");
WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement");
@@ -211,6 +228,31 @@ public partial class SettingsWindow
ApplyUpdateLocalization();
UpdateWallpaperDisplay();
RenderLauncherHiddenItemsList();
UpdateCurrentSettingsPageHeader(_selectedSettingsTabTag);
}
private void UpdateCurrentSettingsPageHeader(string? tag)
{
if (CurrentSettingsPageTitleTextBlock is null || CurrentSettingsPageSubtitleTextBlock is null)
{
return;
}
var pageTag = string.IsNullOrWhiteSpace(tag) ? "General" : NormalizeSettingsPageTag(tag);
CurrentSettingsPageTitleTextBlock.Text = _settingsPageDefinitions.TryGetValue(pageTag, out var definition)
? definition.Title
: L("settings.shell.title", "Settings");
CurrentSettingsPageSubtitleTextBlock.Text = GetSettingsPageDescription(pageTag);
}
private string GetSettingsPageDescription(string tag)
{
if (_settingsPageDefinitions.TryGetValue(tag, out var definition))
{
return definition.Description;
}
return L("settings.shell.sidebar_hint", "Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm.");
}
private void SetAppRenderModeComboItemContent(string tag, string content)

View File

@@ -22,6 +22,42 @@ namespace LanMountainDesktop.Views;
public partial class SettingsWindow
{
private bool TrySetWallpaperPreviewVideoVisibility(bool isVisible)
{
var previewImage = OptionalWallpaperPreviewVideoImage;
if (previewImage is null)
{
return false;
}
previewImage.IsVisible = isVisible;
return true;
}
private bool TrySetWallpaperPreviewVideoSource(IImage? source)
{
var previewImage = OptionalWallpaperPreviewVideoImage;
if (previewImage is null)
{
return false;
}
previewImage.Source = source;
return true;
}
private bool TrySetWallpaperPreviewViewportBackground(IBrush background)
{
var viewport = OptionalWallpaperPreviewViewport;
if (viewport is null)
{
return false;
}
viewport.Background = background;
return true;
}
private void OnNightModeChecked(object? sender, RoutedEventArgs e)
{
if (_suppressThemeToggleEvents)
@@ -139,6 +175,12 @@ public partial class SettingsWindow
private void OnWallpaperPlacementSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (_suppressWallpaperPlacementEvents)
{
return;
}
_selectedWallpaperPlacement = GetWallpaperPlacementFromIndex(WallpaperPlacementComboBox.SelectedIndex);
ApplyWallpaperBrush();
if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null)
{
@@ -157,9 +199,10 @@ public partial class SettingsWindow
{
if (_wallpaperMediaType == WallpaperMediaType.Video && !string.IsNullOrWhiteSpace(_wallpaperVideoPath))
{
DesktopWallpaperLayer.Background = Brushes.Transparent;
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
SyncVideoWallpaperPreviewPlayback();
DesktopWallpaperLayer.Background = GetThemeDefaultDesktopBackground();
TrySetWallpaperPreviewViewportBackground(GetThemeDefaultDesktopBackground());
TrySetWallpaperPreviewVideoSource(null);
TrySetWallpaperPreviewVideoVisibility(false);
return;
}
@@ -168,43 +211,24 @@ public partial class SettingsWindow
{
var fallbackBackground = GetThemeDefaultDesktopBackground();
DesktopWallpaperLayer.Background = fallbackBackground;
WallpaperPreviewViewport.Background = fallbackBackground;
TrySetWallpaperPreviewViewportBackground(fallbackBackground);
return;
}
var placement = GetSelectedWallpaperPlacement();
DesktopWallpaperLayer.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, false);
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, placement, true);
TrySetWallpaperPreviewViewportBackground(CreateWallpaperBrush(_wallpaperBitmap, placement, true));
}
private void SyncVideoWallpaperPreviewPlayback()
{
var shouldPlay =
_wallpaperMediaType == WallpaperMediaType.Video &&
!string.IsNullOrWhiteSpace(_wallpaperVideoPath) &&
WallpaperSettingsPanel.IsVisible;
if (!shouldPlay)
if (_previewVideoWallpaperPlayer?.IsPlaying == true)
{
if (_previewVideoWallpaperPlayer?.IsPlaying == true)
{
StopPreviewVideoCapture(clearSnapshot: false);
}
WallpaperPreviewVideoImage.IsVisible = WallpaperPreviewVideoImage.Source is not null && WallpaperSettingsPanel.IsVisible;
return;
StopPreviewVideoCapture(clearSnapshot: false);
}
if (WallpaperPreviewVideoImage.Source is not null)
{
WallpaperPreviewVideoImage.IsVisible = true;
return;
}
if (_previewVideoWallpaperMedia is null || _previewVideoSnapshotPending)
{
PlayVideoWallpaper(_wallpaperVideoPath!);
}
TrySetWallpaperPreviewVideoSource(null);
TrySetWallpaperPreviewVideoVisibility(false);
}
private void UpdateWallpaperDisplay()
@@ -217,17 +241,17 @@ public partial class SettingsWindow
if (_wallpaperMediaType == WallpaperMediaType.Video)
{
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
TrySetWallpaperPreviewViewportBackground(GetThemeDefaultDesktopBackground());
return;
}
if (_wallpaperBitmap is null)
{
WallpaperPreviewViewport.Background = GetThemeDefaultDesktopBackground();
TrySetWallpaperPreviewViewportBackground(GetThemeDefaultDesktopBackground());
return;
}
WallpaperPreviewViewport.Background = CreateWallpaperBrush(_wallpaperBitmap, GetSelectedWallpaperPlacement(), true);
TrySetWallpaperPreviewViewportBackground(CreateWallpaperBrush(_wallpaperBitmap, GetSelectedWallpaperPlacement(), true));
}
private ImageBrush CreateWallpaperBrush(Bitmap bitmap, WallpaperPlacement placement, bool forPreview)
@@ -272,7 +296,12 @@ public partial class SettingsWindow
private WallpaperPlacement GetSelectedWallpaperPlacement()
{
return WallpaperPlacementComboBox.SelectedIndex switch
return _selectedWallpaperPlacement;
}
private static WallpaperPlacement GetWallpaperPlacementFromIndex(int selectedIndex)
{
return selectedIndex switch
{
1 => WallpaperPlacement.Fit,
2 => WallpaperPlacement.Stretch,
@@ -497,7 +526,7 @@ public partial class SettingsWindow
private void StopPreviewVideoCapture(bool clearSnapshot)
{
WallpaperPreviewVideoImage.IsVisible = false;
TrySetWallpaperPreviewVideoVisibility(false);
_previewVideoWallpaperPlayer?.Stop();
StopPreviewVideoFrameRefreshTimer();
_previewVideoWallpaperMedia?.Dispose();
@@ -517,8 +546,14 @@ public partial class SettingsWindow
return false;
}
var hostWidth = Math.Max(1, WallpaperPreviewViewport.Bounds.Width);
var hostHeight = Math.Max(1, WallpaperPreviewViewport.Bounds.Height);
var previewViewport = OptionalWallpaperPreviewViewport;
if (previewViewport is null)
{
return false;
}
var hostWidth = Math.Max(1, previewViewport.Bounds.Width);
var hostHeight = Math.Max(1, previewViewport.Bounds.Height);
var pixelWidth = Math.Max(1, (int)Math.Round(hostWidth * RenderScaling));
var pixelHeight = Math.Max(1, (int)Math.Round(hostHeight * RenderScaling));
const int maxPixelCount = 1280 * 720;
@@ -570,7 +605,7 @@ public partial class SettingsWindow
(uint)_previewVideoFrameWidth,
(uint)_previewVideoFrameHeight,
(uint)_previewVideoFramePitch);
WallpaperPreviewVideoImage.Source = _previewVideoBitmap;
TrySetWallpaperPreviewVideoSource(_previewVideoBitmap);
return true;
}
catch
@@ -649,24 +684,25 @@ public partial class SettingsWindow
Marshal.Copy(_previewVideoStagingBuffer, sourceOffset, destinationPtr, bytesPerRow);
}
if (!ReferenceEquals(WallpaperPreviewVideoImage.Source, _previewVideoBitmap))
var previewImage = OptionalWallpaperPreviewVideoImage;
if (previewImage is not null && !ReferenceEquals(previewImage.Source, _previewVideoBitmap))
{
WallpaperPreviewVideoImage.Source = _previewVideoBitmap;
previewImage.Source = _previewVideoBitmap;
}
if (_previewVideoSnapshotPending)
{
_previewVideoSnapshotPending = false;
WallpaperPreviewVideoImage.IsVisible = WallpaperSettingsPanel.IsVisible;
TrySetWallpaperPreviewVideoVisibility(IsWallpaperSettingsPageVisible);
StopPreviewVideoCapture(clearSnapshot: false);
WallpaperPreviewVideoImage.IsVisible = WallpaperSettingsPanel.IsVisible;
TrySetWallpaperPreviewVideoVisibility(IsWallpaperSettingsPageVisible);
}
}
private void ReleasePreviewVideoRendererResources()
{
Interlocked.Exchange(ref _previewVideoFrameDirtyFlag, 0);
WallpaperPreviewVideoImage.Source = null;
TrySetWallpaperPreviewVideoSource(null);
_previewVideoBitmap?.Dispose();
_previewVideoBitmap = null;
_previewVideoStagingBuffer = null;
@@ -715,7 +751,7 @@ public partial class SettingsWindow
_previewVideoWallpaperMedia = new Media(_libVlc, new Uri(videoPath));
_previewVideoWallpaperMedia.AddOption(":input-repeat=65535");
_previewVideoSnapshotPending = true;
WallpaperPreviewVideoImage.IsVisible = false;
TrySetWallpaperPreviewVideoVisibility(false);
_previewVideoWallpaperPlayer.Play(_previewVideoWallpaperMedia);
StartPreviewVideoFrameRefreshTimer();
}

View File

@@ -1,312 +1,345 @@
<Window xmlns="https://github.com/avaloniaui"
<local:IndependentSettingsModuleWindowBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent"
xmlns:pages="using:LanMountainDesktop.Views.SettingsPages"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:comp="using:LanMountainDesktop.Views.Components"
xmlns:local="using:LanMountainDesktop.Views"
xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia"
x:Class="LanMountainDesktop.Views.SettingsWindow"
x:Name="IndependentSettingsModuleWindow"
Title="Settings"
Icon="/Assets/avalonia-logo.ico"
Width="1520"
Height="960"
MinWidth="1240"
MinHeight="820"
Width="1240"
Height="860"
MinWidth="980"
MinHeight="680"
ShowInTaskbar="True"
WindowStartupLocation="CenterScreen"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}">
SystemDecorations="None"
CanResize="True"
UseLayoutRounding="True"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
windowing:AppWindow.AllowInteractionInTitleBar="True"
FontFamily="Segoe UI Variable Text, {DynamicResource AppFontFamily}">
<Window.Styles>
<Style Selector="Border.settings-shell-card">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Style Selector="Border.independent-settings-shell">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="28" />
<Setter Property="BoxShadow" Value="0 10 28 #12000000" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="BoxShadow" Value="0 12 36 #22000000" />
</Style>
<Style Selector="TextBlock.settings-shell-eyebrow">
<Setter Property="FontSize" Value="12" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Style Selector="TextBlock.independent-settings-hint">
<Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
<Setter Property="FontSize" Value="12.5" />
</Style>
<Style Selector="TextBlock.settings-shell-hint">
<Setter Property="FontSize" Value="13" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" />
<Style Selector="Border.independent-settings-titlebar">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorSecondaryBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style>
<Style Selector="StackPanel.settings-sidebar-host Button.settings-sidebar-item">
<Style Selector="Button.independent-settings-titlebar-button">
<Setter Property="Width" Value="46" />
<Setter Property="Height" Value="32" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="18" />
<Setter Property="Padding" Value="14,12" />
<Setter Property="Margin" Value="0,0,0,8" />
<Setter Property="HorizontalContentAlignment" Value="Stretch" />
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
<BrushTransition Property="BorderBrush" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
<TransformOperationsTransition Property="RenderTransform" Duration="{StaticResource FluttermotionToken.Duration.Fast}" Easing="0.22,1,0.36,1" />
</Transitions>
</Setter>
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
</Style>
<Style Selector="StackPanel.settings-sidebar-host Button.settings-sidebar-item:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="RenderTransform" Value="scale(1.01)" />
<Style Selector="Button.independent-settings-titlebar-button:pointerover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
<Style Selector="StackPanel.settings-sidebar-host Button.settings-sidebar-item.nav-selected">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveAccentBrush}" />
</Style>
<Style Selector="Border.settings-sidebar-icon-shell">
<Setter Property="Width" Value="34" />
<Setter Property="Height" Value="34" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonBackgroundBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
</Style>
<Style Selector="Button.settings-sidebar-item.nav-selected Border.settings-sidebar-icon-shell">
<Setter Property="Background" Value="{DynamicResource AdaptiveAccentBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveAccentBrush}" />
</Style>
<Style Selector="TextBlock.settings-nav-label">
<Setter Property="FontSize" Value="16" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="ic|SymbolIcon.settings-nav-icon">
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
<Setter Property="FontSize" Value="18" />
<Setter Property="HorizontalAlignment" Value="Center" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Button.settings-sidebar-item.nav-selected ic|SymbolIcon.settings-nav-icon">
<Style Selector="Button.independent-settings-titlebar-close:pointerover">
<Setter Property="Background" Value="#CCB91C1C" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="Button.independent-settings-pane-button">
<Setter Property="Width" Value="40" />
<Setter Property="Height" Value="40" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
</Style>
<Style Selector="Button.independent-settings-pane-button:pointerover">
<Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
</Style>
<Style Selector="ui|NavigationView#SettingsNavView">
<Setter Property="PaneDisplayMode" Value="Auto" />
<Setter Property="IsBackButtonVisible" Value="False" />
<Setter Property="IsPaneToggleButtonVisible" Value="False" />
<Setter Property="IsSettingsVisible" Value="False" />
<Setter Property="OpenPaneLength" Value="283" />
<Setter Property="CompactPaneLength" Value="0" />
<Setter Property="AlwaysShowHeader" Value="False" />
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ui|NavigationViewItem">
<Setter Property="Margin" Value="6,2" />
</Style>
<Style Selector="ui|NavigationViewItemHeader">
<Setter Property="Margin" Value="14,14,0,4" />
</Style>
<Style Selector="ui|InfoBar#IndependentSettingsStatusInfoBar">
<Setter Property="Margin" Value="0,0,0,14" />
</Style>
<Style Selector="Border.settings-page-shell">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseAltBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="16" />
<Setter Property="Padding" Value="24" />
</Style>
</Window.Styles>
<Grid x:Name="DesktopHost">
<Grid x:Name="DesktopHost"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}">
<Border x:Name="DesktopWallpaperLayer"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" />
IsVisible="False"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Grid x:Name="SettingsPage"
Classes="settings-scope"
IsVisible="True"
Opacity="1"
Margin="20">
<Grid x:Name="SettingsContentPanel"
RowDefinitions="Auto,*"
RowSpacing="18">
<Border Margin="12"
Classes="independent-settings-shell">
<Grid RowDefinitions="48,*">
<Border Grid.Row="0"
Classes="settings-shell-card"
Padding="20,18">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="18">
<Border Width="52"
Height="52"
CornerRadius="18"
Background="{DynamicResource AdaptiveAccentBrush}">
<TextBlock Text="LMD"
FontSize="16"
FontWeight="Bold"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
Classes="independent-settings-titlebar"
PointerPressed="OnTitleBarPointerPressed"
DoubleTapped="OnTitleBarDoubleTapped">
<Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
ColumnSpacing="10"
Margin="10,0,8,0">
<Button x:Name="SettingsPaneToggleButton"
Grid.Column="0"
Classes="independent-settings-pane-button"
Click="OnSettingsPaneToggleButtonClick"
ToolTip.Tip="Toggle navigation">
<fi:SymbolIcon Symbol="Navigation" />
</Button>
<StackPanel Grid.Column="1"
Spacing="3"
VerticalAlignment="Center">
<TextBlock x:Name="WindowTitleTextBlock"
FontSize="28"
FontWeight="SemiBold"
Text="Application Settings" />
<TextBlock x:Name="WindowSubtitleTextBlock"
Classes="settings-shell-hint"
Text="LanMountainDesktop" />
</StackPanel>
<StackPanel Grid.Column="2"
Orientation="Horizontal"
Spacing="10"
VerticalAlignment="Center">
<Border Classes="settings-shell-card"
Padding="12,8"
CornerRadius="18">
<Border Width="28"
Height="28"
CornerRadius="9"
Background="{DynamicResource AccentFillColorDefaultBrush}">
<TextBlock Text="L"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
FontWeight="Bold"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Spacing="0"
VerticalAlignment="Center">
<TextBlock x:Name="WindowTitleTextBlock"
FontSize="13"
FontWeight="SemiBold"
Text="Settings" />
<TextBlock x:Name="WindowSubtitleTextBlock"
Classes="independent-settings-hint"
Text="LanMountainDesktop independent settings module" />
</StackPanel>
</StackPanel>
<StackPanel Grid.Column="3"
Orientation="Horizontal"
Spacing="8"
VerticalAlignment="Center"
HorizontalAlignment="Right">
<Border Classes="settings-page-shell"
Padding="10,4"
CornerRadius="12">
<TextBlock x:Name="WindowVersionBadgeTextBlock"
FontSize="14"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
FontWeight="SemiBold"
Text="1.0.0" />
</Border>
<Border Classes="settings-shell-card"
Padding="12,8"
CornerRadius="18">
<Border Classes="settings-page-shell"
Padding="10,4"
CornerRadius="12">
<TextBlock x:Name="WindowCodeNameBadgeTextBlock"
Classes="settings-shell-hint"
Classes="independent-settings-hint"
Text="Administrate" />
</Border>
<Button Classes="independent-settings-pane-button"
ToolTip.Tip="More options">
<fi:SymbolIcon Symbol="MoreHorizontal" />
<Button.Flyout>
<MenuFlyout>
<MenuItem Header="Open Logs Folder"
Click="OnOpenLogsFolderClick">
<MenuItem.Icon>
<fi:SymbolIcon Symbol="FolderOpen" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Open App Folder"
Click="OnOpenAppFolderClick">
<MenuItem.Icon>
<fi:SymbolIcon Symbol="FolderOpen" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button>
</StackPanel>
<StackPanel Grid.Column="4"
Orientation="Horizontal"
Spacing="4"
VerticalAlignment="Center">
<Button Classes="independent-settings-titlebar-button"
Click="OnMinimizeWindowClick">
<fi:SymbolIcon Symbol="Subtract" />
</Button>
<Button Classes="independent-settings-titlebar-button"
Click="OnToggleWindowStateClick">
<fi:SymbolIcon x:Name="WindowStateToggleIcon"
Symbol="Square" />
</Button>
<Button Classes="independent-settings-titlebar-button independent-settings-titlebar-close"
Click="OnCloseWindowClick">
<fi:SymbolIcon Symbol="Dismiss" />
</Button>
</StackPanel>
</Grid>
</Border>
<Grid Grid.Row="1"
ColumnDefinitions="300,20,*">
<Border Grid.Column="0"
Classes="settings-shell-card"
Padding="18,18,18,16">
<Grid RowDefinitions="Auto,*,Auto"
RowSpacing="18">
<StackPanel Spacing="6">
<Grid Grid.Row="1">
<ui:NavigationView x:Name="SettingsNavView"
SelectionChanged="OnSettingsNavSelectionChanged">
<ui:NavigationView.PaneHeader>
<StackPanel Margin="10,10,10,12"
Spacing="4">
<TextBlock x:Name="SettingsSidebarTitleTextBlock"
Classes="settings-shell-eyebrow"
FontSize="15"
FontWeight="SemiBold"
Text="Settings" />
<TextBlock x:Name="SettingsSidebarHintTextBlock"
Classes="settings-shell-hint"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Choose a category to adjust application behavior and desktop appearance." />
Text="Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm." />
</StackPanel>
</ui:NavigationView.PaneHeader>
<ScrollViewer Grid.Row="1"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="20">
<StackPanel Spacing="10">
<TextBlock x:Name="SettingsPrimaryGroupTextBlock"
Classes="settings-shell-eyebrow"
Text="Desktop" />
<StackPanel x:Name="SettingsPrimaryNavHost"
Classes="settings-sidebar-host"
Spacing="0" />
</StackPanel>
<Border Background="{DynamicResource SurfaceStrokeColorDefaultBrush}"
Height="1" />
<StackPanel Spacing="10">
<TextBlock x:Name="SettingsSecondaryGroupTextBlock"
Classes="settings-shell-eyebrow"
Text="System" />
<StackPanel x:Name="SettingsSecondaryNavHost"
Classes="settings-sidebar-host"
Spacing="0" />
</StackPanel>
<StackPanel x:Name="SettingsPluginNavSection"
IsVisible="False"
Spacing="10">
<Border Background="{DynamicResource SurfaceStrokeColorDefaultBrush}"
Height="1" />
<TextBlock x:Name="SettingsPluginGroupTextBlock"
Classes="settings-shell-eyebrow"
Text="Extensions" />
<StackPanel x:Name="SettingsPluginNavHost"
Classes="settings-sidebar-host"
Spacing="0" />
</StackPanel>
</StackPanel>
</ScrollViewer>
<Border Grid.Row="2"
Classes="settings-shell-card"
Padding="14,12"
CornerRadius="22">
<StackPanel Spacing="4">
<TextBlock Text="LanMountainDesktop"
FontWeight="SemiBold" />
<TextBlock x:Name="SettingsSidebarFooterTextBlock"
Classes="settings-shell-hint"
TextWrapping="Wrap"
Text="Tray-opened settings are managed in this standalone window." />
</StackPanel>
<ui:NavigationView.PaneFooter>
<Border Margin="10,10,10,12"
Classes="settings-page-shell"
Padding="12">
<TextBlock x:Name="SettingsSidebarFooterTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Both the tray entry and the in-app settings entry now open this same independent settings module." />
</Border>
</Grid>
</Border>
</ui:NavigationView.PaneFooter>
<Grid Grid.Column="2"
RowDefinitions="*,Auto"
RowSpacing="14">
<Border Grid.Row="0"
Classes="settings-shell-card"
Padding="0">
<ScrollViewer x:Name="SettingsContentScrollViewer"
Padding="30,28,30,30"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<Grid x:Name="SettingsContentPagesHost">
<pages:WallpaperSettingsPage x:Name="WallpaperSettingsPanel" IsVisible="True" />
<pages:GridSettingsPage x:Name="GridSettingsPanel" IsVisible="False" />
<pages:ColorSettingsPage x:Name="ColorSettingsPanel" IsVisible="False" />
<pages:StatusBarSettingsPage x:Name="StatusBarSettingsPanel" IsVisible="False" />
<pages:WeatherSettingsPage x:Name="WeatherSettingsPanel" IsVisible="False" />
<pages:RegionSettingsPage x:Name="RegionSettingsPanel" IsVisible="False" />
<pages:UpdateSettingsPage x:Name="UpdateSettingsPanel" IsVisible="False" />
<pages:LauncherSettingsPage x:Name="LauncherSettingsPanel" IsVisible="False" />
<pages:AboutSettingsPage x:Name="AboutSettingsPanel" IsVisible="False" />
<pages:PluginSettingsPage x:Name="PluginSettingsPanel" IsVisible="False" />
<pages:PluginMarketSettingsPage x:Name="PluginMarketSettingsPanel" IsVisible="False" />
</Grid>
</ScrollViewer>
</Border>
<Grid RowDefinitions="Auto,*">
<ui:InfoBar x:Name="IndependentSettingsStatusInfoBar"
Title="Independent settings module status"
IsClosable="True"
IsOpen="False"
Severity="Warning" />
<Border x:Name="PendingRestartDock"
Grid.Row="1"
IsVisible="False"
Classes="settings-shell-card"
Padding="16,14"
CornerRadius="24">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="14">
<Border Width="38"
Height="38"
CornerRadius="14"
Background="{DynamicResource AdaptiveAccentBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="18"
Foreground="White"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="14"
<Grid Grid.Row="1"
Margin="18,4,18,18"
RowDefinitions="Auto,*,Auto">
<StackPanel Spacing="4"
Margin="4,0,0,16"
MaxWidth="980"
HorizontalAlignment="Left">
<TextBlock x:Name="CurrentSettingsPageTitleTextBlock"
FontSize="28"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
Text="General" />
<TextBlock x:Name="CurrentSettingsPageSubtitleTextBlock"
Classes="independent-settings-hint"
MaxWidth="720"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Your changes will apply after restarting the app." />
Text="Configure this part of LanMountainDesktop in the independent settings module." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="16,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
</StackPanel>
</Button>
<Border Grid.Row="1"
Classes="settings-page-shell"
MaxWidth="980"
HorizontalAlignment="Left">
<ScrollViewer x:Name="SettingsContentScrollViewer"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<ui:Frame x:Name="SettingsPageFrame"
Margin="0" />
</ScrollViewer>
</Border>
<Border x:Name="PendingRestartDock"
Grid.Row="2"
IsVisible="False"
Margin="0,14,0,0"
Classes="settings-page-shell"
Padding="14,12"
MaxWidth="980"
HorizontalAlignment="Left">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="12">
<Border Width="36"
Height="36"
CornerRadius="18"
Background="{DynamicResource AccentFillColorDefaultBrush}">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular"
FontSize="16"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1"
Spacing="2"
VerticalAlignment="Center">
<TextBlock x:Name="PendingRestartDockTitleTextBlock"
FontSize="14"
FontWeight="SemiBold"
Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap"
Text="Your changes will apply after restarting the app." />
</StackPanel>
<Button x:Name="PendingRestartDockButton"
Grid.Column="2"
Padding="14,8"
Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" />
<TextBlock x:Name="PendingRestartDockButtonTextBlock"
VerticalAlignment="Center"
Text="Restart app" />
</StackPanel>
</Button>
</Grid>
</Border>
</Grid>
</Border>
</Grid>
</Grid>
</ui:NavigationView>
</Grid>
</Grid>
</Grid>
</Border>
<Grid IsVisible="False">
<Button x:Name="BackToWindowsButton" />
@@ -324,4 +357,4 @@
<Border x:Name="BottomTaskbarContainer" />
</Grid>
</Grid>
</Window>
</local:IndependentSettingsModuleWindowBase>

View File

@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
@@ -22,12 +25,13 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
using LibVLCSharp.Shared;
using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views;
public partial class SettingsWindow : Window
public partial class SettingsWindow : IndependentSettingsModuleWindowBase
{
private enum WallpaperPlacement
{
@@ -102,8 +106,23 @@ public partial class SettingsWindow : Window
private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
private readonly Dictionary<string, Button> _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Button> _pluginSettingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, NavigationViewItem> _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, NavigationViewItem> _pluginSettingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, IndependentSettingsPageDefinition> _settingsPageDefinitions = new(StringComparer.OrdinalIgnoreCase);
private GeneralSettingsPage? GeneralSettingsHubPanel;
private AppearanceSettingsPage? AppearanceSettingsHubPanel;
private ComponentsSettingsPage? ComponentsSettingsHubPanel;
private WallpaperSettingsPage? WallpaperSettingsPanel;
private GridSettingsPage? GridSettingsPanel;
private ColorSettingsPage? ColorSettingsPanel;
private StatusBarSettingsPage? StatusBarSettingsPanel;
private WeatherSettingsPage? WeatherSettingsPanel;
private RegionSettingsPage? RegionSettingsPanel;
private UpdateSettingsPage? UpdateSettingsPanel;
private LauncherSettingsPage? LauncherSettingsPanel;
private AboutSettingsPage? AboutSettingsPanel;
private PluginSettingsPage? PluginSettingsPanel;
private PluginMarketSettingsPage? PluginMarketSettingsPanel;
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
@@ -168,20 +187,28 @@ public partial class SettingsWindow : Window
private bool _weatherNoTlsRequests;
private bool _autoStartWithWindows;
private string _weatherSearchKeyword = string.Empty;
private string _selectedSettingsTabTag = "Wallpaper";
private string _selectedSettingsTabTag = "General";
private WallpaperPlacement _selectedWallpaperPlacement = WallpaperPlacement.Fill;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
private bool _controlsBound;
private bool _independentModuleInitializationCompleted;
private bool _suppressWallpaperPlacementEvents;
private bool _isIndependentSettingsModuleClosing;
private bool _allowIndependentSettingsModuleRealClose;
public SettingsWindow()
{
_componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService);
InitializeComponent();
InitializeSettingsPageHosts();
InitializeSettingsNavigation();
InitializePluginSettingsNavigation();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
HookEvents();
Closing += OnIndependentSettingsModuleClosing;
Opened += OnWindowOpened;
}
private void InitializeComponent()
@@ -235,7 +262,27 @@ public partial class SettingsWindow : Window
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled;
AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged;
Opened += OnWindowOpened;
}
private void EnsureIndependentModuleControlsBound()
{
if (_controlsBound)
{
return;
}
AppLogger.Info("IndependentSettingsModule", "ControlsBindingStarted.");
try
{
HookEvents();
_controlsBound = true;
AppLogger.Info("IndependentSettingsModule", "ControlsBindingCompleted.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn("IndependentSettingsModule", "ControlsBindingFailed.", ex);
throw new InvalidOperationException("Failed to bind independent settings module controls.", ex);
}
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
@@ -273,69 +320,287 @@ public partial class SettingsWindow : Window
private void OnWindowOpened(object? sender, EventArgs e)
{
Opened -= OnWindowOpened;
_suppressSettingsPersistence = true;
var snapshot = _appSettingsService.Load();
var launcherSnapshot = _launcherSettingsService.Load();
UpdateWindowChromeState();
UiExceptionGuard.FireAndForgetGuarded(
async () =>
{
EnsureIndependentModuleControlsBound();
await InitializeIndependentSettingsModuleAsync();
},
"IndependentSettingsModule.Initialize",
UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
ex =>
{
ShowIndependentModuleStatus(
L("settings.shell.init_failed_title", "设置模块初始化失败"),
ex.Message,
InfoBarSeverity.Warning);
return Task.CompletedTask;
});
}
_targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
private void OnIndependentSettingsModuleClosing(object? sender, WindowClosingEventArgs e)
{
AppLogger.Info(
"IndependentSettingsModule",
$"CloseRequested; AllowRealClose={_allowIndependentSettingsModuleRealClose}; Reason='{e.CloseReason}'.");
if (!_allowIndependentSettingsModuleRealClose &&
e.CloseReason is not WindowCloseReason.ApplicationShutdown &&
e.CloseReason is not WindowCloseReason.OSShutdown)
{
"Compact" => 0,
"Custom" => 2,
_ => 1
};
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeAppRenderModeSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeSettingsIcons();
ApplyLocalization();
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
TryRestoreWallpaper(snapshot.WallpaperPath);
RefreshColorPalettes();
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
{
_selectedThemeColor = savedThemeColor;
e.Cancel = true;
PersistSettings();
Hide();
AppLogger.Info("IndependentSettingsModule", "WindowHiddenByClose.");
return;
}
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: false);
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
UpdateWallpaperDisplay();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
InitializeTimeZoneSettings();
_ = LoadLauncherEntriesAsync();
_suppressSettingsPersistence = false;
_isIndependentSettingsModuleClosing = true;
}
private async Task InitializeIndependentSettingsModuleAsync()
{
if (_independentModuleInitializationCompleted)
{
return;
}
AppLogger.Info("IndependentSettingsModule", "ModuleInitStarted; Stage='Opened'.");
_suppressSettingsPersistence = true;
try
{
ShowIndependentModuleStatus(string.Empty, string.Empty, InfoBarSeverity.Informational, isOpen: false);
var snapshot = new AppSettingsSnapshot();
var launcherSnapshot = new LauncherSettingsSnapshot();
await RunInitializationStageAsync("SnapshotLoad", () =>
{
snapshot = _appSettingsService.Load();
launcherSnapshot = _launcherSettingsService.Load();
return Task.CompletedTask;
});
await RunInitializationStageAsync("BaseConfiguration", () =>
{
_targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells,
MaxShortSideCells);
_gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
_desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
_statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
_statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
GridSizeNumberBox.Value = _targetShortSideCells;
GridSizeSlider.Value = _targetShortSideCells;
GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
{
"Compact" => 0,
"Custom" => 2,
_ => 1
};
StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode);
InitializeWeatherSettings(snapshot);
InitializeAutoStartWithWindowsSetting(snapshot);
InitializeAppRenderModeSetting(snapshot);
InitializeUpdateSettings(snapshot);
InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeSettingsIcons();
ApplyLocalization();
return Task.CompletedTask;
});
await RunInitializationStageAsync("VisualState", () =>
{
_selectedWallpaperPlacement = GetWallpaperPlacementFromIndex(GetPlacementIndexFromSetting(snapshot.WallpaperPlacement));
_suppressWallpaperPlacementEvents = true;
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
_suppressWallpaperPlacementEvents = false;
TryRestoreWallpaper(snapshot.WallpaperPath);
RefreshColorPalettes();
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
{
_selectedThemeColor = savedThemeColor;
}
_isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
ApplyNightModeState(_isNightMode, refreshPalettes: false);
EnsureSelectedThemeColor();
UpdateThemeColorSelectionState();
ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
_defaultDesktopBackground = DesktopWallpaperLayer.Background;
RestoreSettingsTabSelection(snapshot);
UpdateSettingsTabContent();
UpdateWallpaperDisplay();
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout();
InitializeTimeZoneSettings();
return Task.CompletedTask;
});
UiExceptionGuard.FireAndForgetGuarded(
LoadLauncherEntriesAsync,
"IndependentSettingsModule.LoadLauncherEntries",
UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
ex =>
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能载入"),
ex.Message,
InfoBarSeverity.Warning);
return Task.CompletedTask;
});
_independentModuleInitializationCompleted = true;
AppLogger.Info("IndependentSettingsModule", "ModuleInitCompleted.");
}
finally
{
_suppressSettingsPersistence = false;
}
}
private async Task RunInitializationStageAsync(string stage, Func<Task> action)
{
AppLogger.Info("IndependentSettingsModule", $"ModuleInitStarted; Stage='{stage}'.");
try
{
await action();
AppLogger.Info("IndependentSettingsModule", $"ModuleInitCompleted; Stage='{stage}'.");
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
AppLogger.Warn("IndependentSettingsModule", $"ModuleInitFailed; Stage='{stage}'.", ex);
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能载入"),
ex.Message,
InfoBarSeverity.Warning);
}
}
private void ShowIndependentModuleStatus(string title, string message, InfoBarSeverity severity, bool isOpen = true)
{
if (IndependentSettingsStatusInfoBar is null)
{
return;
}
IndependentSettingsStatusInfoBar.Title = title;
IndependentSettingsStatusInfoBar.Message = message;
IndependentSettingsStatusInfoBar.Severity = severity;
IndependentSettingsStatusInfoBar.IsOpen = isOpen;
}
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
BeginMoveDrag(e);
}
}
private void OnTitleBarDoubleTapped(object? sender, RoutedEventArgs e)
{
if (!CanResize)
{
return;
}
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
UpdateWindowChromeState();
}
private void OnMinimizeWindowClick(object? sender, RoutedEventArgs e)
{
WindowState = WindowState.Minimized;
UpdateWindowChromeState();
}
private void OnToggleWindowStateClick(object? sender, RoutedEventArgs e)
{
if (!CanResize)
{
return;
}
WindowState = WindowState == WindowState.Maximized
? WindowState.Normal
: WindowState.Maximized;
UpdateWindowChromeState();
}
private void UpdateWindowChromeState()
{
if (WindowStateToggleIcon is null)
{
return;
}
WindowStateToggleIcon.Symbol = WindowState == WindowState.Maximized
? FluentIcons.Common.Symbol.SquareMultiple
: FluentIcons.Common.Symbol.Square;
}
private void OnCloseWindowClick(object? sender, RoutedEventArgs e)
{
Close();
}
private void OnSettingsPaneToggleButtonClick(object? sender, RoutedEventArgs e)
{
if (SettingsNavView is not null)
{
SettingsNavView.IsPaneOpen = !SettingsNavView.IsPaneOpen;
}
}
private void OnOpenLogsFolderClick(object? sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = AppLogger.LogDirectory,
UseShellExecute = true
});
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
ex.Message,
InfoBarSeverity.Warning);
}
}
private void OnOpenAppFolderClick(object? sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo
{
FileName = Path.GetFullPath(".") ?? string.Empty,
UseShellExecute = true
});
}
catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
{
ShowIndependentModuleStatus(
L("settings.shell.partial_warning_title", "部分内容未能加载"),
ex.Message,
InfoBarSeverity.Warning);
}
}
}

View File

@@ -145,22 +145,29 @@ public sealed class PluginLoader
{
Directory.CreateDirectory(dataDirectory);
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
AppLogger.Info(
"PluginLoader",
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames);
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
AppLogger.Info("PluginLoader", $"Assembly loaded. PluginId='{manifest.Id}'; Assembly='{assembly.FullName}'.");
var pluginType = ResolvePluginType(assembly);
plugin = CreatePluginInstance(pluginType);
AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
var serviceCollection = CreateServiceCollection(runtimeContext, services);
var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
plugin.Initialize(hostBuilderContext, serviceCollection);
AppLogger.Info("PluginLoader", $"Plugin Initialize completed. PluginId='{manifest.Id}'.");
pluginServices = serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = false,
ValidateOnBuild = true
});
AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices
@@ -174,8 +181,12 @@ public sealed class PluginLoader
.ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var exportedServices = ResolveExports(manifest, pluginServices);
AppLogger.Info(
"PluginLoader",
$"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsPages={settingsPages.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
StartHostedServices(hostedServices);
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
var loadedPlugin = new LoadedPlugin(
manifest,
@@ -375,6 +386,7 @@ public sealed class PluginLoader
{
foreach (var hostedService in hostedServices)
{
AppLogger.Info("PluginLoader", $"Starting hosted service '{hostedService.GetType().FullName}'.");
hostedService.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
}
}

View File

@@ -32,7 +32,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
try
{
var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken);
var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken).ConfigureAwait(false);
var document = AirAppMarketIndexDocument.Load(json, localIndexPath);
_cacheService.SaveIndexJson(json);
return new AirAppMarketLoadResult(
@@ -61,8 +61,8 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
using var response = await _httpClient.GetAsync(
AirAppMarketDefaults.DefaultIndexUrl,
cancellationToken);
var json = await response.Content.ReadAsStringAsync(cancellationToken);
cancellationToken).ConfigureAwait(false);
var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var document = AirAppMarketIndexDocument.Load(json, AirAppMarketDefaults.DefaultIndexUrl);

View File

@@ -31,14 +31,8 @@ internal static class AirAppMarketDefaults
public static string? TryGetWorkspaceIndexPath()
{
var repositoryRoot = TryGetWorkspaceRepositoryRoot("LanAirApp");
if (repositoryRoot is null)
{
return null;
}
var candidatePath = Path.Combine(repositoryRoot, "airappmarket", "index.json");
return File.Exists(candidatePath) ? candidatePath : null;
var relativePath = Path.Combine("airappmarket", "index.json");
return TryResolveWorkspacePath("LanAirApp", relativePath);
}
public static bool TryResolveWorkspaceFile(string url, out string localPath)
@@ -57,14 +51,8 @@ internal static class AirAppMarketDefaults
return false;
}
var repositoryRoot = TryGetWorkspaceRepositoryRoot(repositoryName);
if (repositoryRoot is null)
{
return false;
}
var candidatePath = Path.GetFullPath(Path.Combine(repositoryRoot, relativePath));
if (!File.Exists(candidatePath))
var candidatePath = TryResolveWorkspacePath(repositoryName, relativePath);
if (candidatePath is null)
{
return false;
}
@@ -99,7 +87,7 @@ internal static class AirAppMarketDefaults
return !string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repositoryName);
}
private static string? TryGetWorkspaceRepositoryRoot(string repositoryName)
private static string? TryResolveWorkspacePath(string repositoryName, string relativePath)
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
@@ -107,7 +95,11 @@ internal static class AirAppMarketDefaults
var candidate = Path.Combine(current.FullName, repositoryName);
if (Directory.Exists(candidate))
{
return candidate;
var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
if (File.Exists(candidatePath))
{
return candidatePath;
}
}
current = current.Parent;

View File

@@ -1,25 +1,43 @@
<UserControl xmlns="https://github.com/avaloniaui"
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d"
d:DesignWidth="960"
d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginMarketSettingsPage">
<StackPanel x:Name="PluginMarketPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginMarketPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugin Market" />
<TextBlock x:Name="PluginMarketPanelSubtitleTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Browse plugins from the official LanAirApp source and stage installs." />
Text="Browse plugins from the official LanAirApp source, review package details, and stage installations safely." />
<ContentControl x:Name="PluginMarketContentHost" />
<Border Classes="settings-expander-shell"
Padding="16,14">
<StackPanel Spacing="10">
<TextBlock x:Name="PluginMarketSectionTitleTextBlock"
FontSize="16"
FontWeight="SemiBold"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Official Source" />
<TextBlock x:Name="PluginMarketSectionHintTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="The content below is loaded from the official market source. If network loading fails, the module will keep the page alive and show a recoverable error state instead of crashing." />
<ContentControl x:Name="PluginMarketContentHost" />
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@@ -66,6 +66,7 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions();
UnloadInstalledPlugins();
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load();
@@ -81,6 +82,9 @@ public sealed class PluginRuntimeService : IDisposable
var discoveryFailures = new List<PluginLoadResult>();
var candidates = DiscoverCandidates(discoveryFailures);
_loadResults.AddRange(discoveryFailures);
AppLogger.Info(
"PluginRuntime",
$"Plugin discovery completed. Candidates={candidates.Count}; DiscoveryFailures={discoveryFailures.Count}; PluginsDirectory='{PluginsDirectory}'.");
var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
@@ -93,6 +97,7 @@ public sealed class PluginRuntimeService : IDisposable
new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
_loadResults.Add(duplicateFailure);
LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
continue;
}
@@ -113,7 +118,13 @@ public sealed class PluginRuntimeService : IDisposable
try
{
AppLogger.Info(
"PluginRuntime",
$"Preparing shared contracts. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
RegisterSharedContractsForLoad(candidate.Manifest);
AppLogger.Info(
"PluginRuntime",
$"Shared contracts ready. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'.");
}
catch (Exception ex)
{
@@ -128,10 +139,13 @@ public sealed class PluginRuntimeService : IDisposable
ex.Message,
0,
0));
Debug.WriteLine($"[PluginRuntime] Failed to prepare dependencies for '{candidate.Manifest.Id}': {ex}");
LogPluginFailure("DependencyPrepare", dependencyFailure, treatAsError: false);
continue;
}
AppLogger.Info(
"PluginRuntime",
$"Starting plugin load. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
var loadResult = candidate.SourceKind switch
{
PluginCatalogSourceKind.Package => _loader.LoadFromPackage(
@@ -160,6 +174,9 @@ public sealed class PluginRuntimeService : IDisposable
null,
loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.DesktopComponents.Count));
AppLogger.Info(
"PluginRuntime",
$"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? "<unknown>"}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? "<unknown>"}'; SourceKind='{candidate.SourceKind}'; SettingsPages={loadResult.LoadedPlugin.SettingsPages.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
@@ -173,11 +190,15 @@ public sealed class PluginRuntimeService : IDisposable
loadResult.Error?.Message,
0,
0));
LogPluginFailure("Load", loadResult, treatAsError: true);
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
}
if (_catalog.Count == 0 && discoveryFailures.Count == 0)
{
AppLogger.Info(
"PluginRuntime",
$"No plugin packages or loose manifests were discovered under '{PluginsDirectory}'.");
Debug.WriteLine($"[PluginRuntime] No .laapp packages or loose plugin manifests found under '{PluginsDirectory}'.");
}
}
@@ -392,7 +413,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(packagePath, null, ex));
var failure = PluginLoadResult.Failure(packagePath, null, ex);
failures.Add(failure);
LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -405,7 +428,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
failures.Add(PluginLoadResult.Failure(manifestPath, null, ex));
var failure = PluginLoadResult.Failure(manifestPath, null, ex);
failures.Add(failure);
LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -717,6 +742,21 @@ public sealed class PluginRuntimeService : IDisposable
return Path.Combine(PluginsDirectory, PendingDeletionFileName);
}
private static void LogPluginFailure(string stage, PluginLoadResult result, bool treatAsError)
{
var manifest = result.Manifest;
var message =
$"Plugin load issue. Stage='{stage}'; PluginId='{manifest?.Id ?? "<unknown>"}'; SourcePath='{result.SourcePath}'; ManifestVersion='{manifest?.Version ?? "<unknown>"}'; ApiVersion='{manifest?.ApiVersion ?? "<unknown>"}'; Error='{result.Error?.Message ?? "<none>"}'.";
if (treatAsError)
{
AppLogger.Error("PluginRuntime", message, result.Error);
return;
}
AppLogger.Warn("PluginRuntime", message, result.Error);
}
private void RemovePluginFromSnapshot(string pluginId)
{
var snapshot = _appSettingsService.Load();

View File

@@ -7,31 +7,40 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginSettingsPage">
<StackPanel x:Name="PluginSettingsPanel" Spacing="16">
<StackPanel x:Name="PluginSettingsPanel"
MaxWidth="920"
Spacing="16">
<TextBlock x:Name="PluginSettingsPanelTitleTextBlock"
IsVisible="False"
FontSize="24"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugins" />
<TextBlock x:Name="PluginSettingsPanelSubtitleTextBlock"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Manage installed plugins, local package import, and runtime availability from one place." />
<Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="PluginSystemSettingsExpander"
Header="Plugin Runtime"
Description="Manage plugin loading and backend isolation."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe734;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="PuzzlePiece"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginSystemDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="This page will host installed plugin management, permission review, and sandboxed backend runtime controls." />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
<Border Background="{DynamicResource LayerFillColorDefaultBrush}"
CornerRadius="{DynamicResource DesignCornerRadiusSm}"
Padding="14">
<TextBlock x:Name="PluginSystemStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin management UI is not connected yet. Next step is wiring the loader, permissions, and worker isolation state into this panel." />
</Border>
@@ -47,16 +56,17 @@
Description="Manage installed plugins here."
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8fd;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="Apps"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
<TextBlock x:Name="PluginRestartHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Plugin enable state changes take effect after restarting the app." />
<TextBlock x:Name="PluginCatalogEmptyTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No plugins found."
IsVisible="False" />
</StackPanel>
@@ -70,7 +80,8 @@
Description="Open a .laapp package and stage it into the local plugin directory."
IsExpanded="False">
<ui:SettingsExpander.IconSource>
<ui:FontIconSource Glyph="&#xe8b7;" FontFamily="{StaticResource SymbolThemeFontFamily}" />
<fi:SymbolIconSource Symbol="ArrowUpload"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<StackPanel Spacing="10">
@@ -79,11 +90,11 @@
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
<TextBlock x:Name="PluginPackageImportHintTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
Text="Open a .laapp package to install it into the local plugin directory." />
<TextBlock x:Name="PluginPackageImportStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap"
IsVisible="False" />
</StackPanel>

View File

@@ -8,6 +8,7 @@ using System.Runtime.Loader;
using System.Security.Cryptography;
using System.Threading;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Plugins;
@@ -48,6 +49,9 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var document = LoadIndex(cancellationToken);
AppLogger.Info(
"PluginSharedContracts",
$"Shared contract index loaded for plugin '{manifest.Id}'. SourceContracts={document.Contracts.Count}.");
foreach (var reference in manifest.SharedContracts)
{
EnsureInstalled(document, reference, cancellationToken);
@@ -64,9 +68,19 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var assemblyNames = new List<string>(manifest.SharedContracts.Count);
AirAppMarketIndexDocument? document = null;
foreach (var reference in manifest.SharedContracts)
{
var assemblyPath = GetInstalledAssemblyPath(reference);
if (!File.Exists(assemblyPath))
{
document ??= LoadIndex(cancellationToken);
AppLogger.Info(
"PluginSharedContracts",
$"Installing missing shared contract during plugin load. PluginId='{manifest.Id}'; ContractId='{reference.Id}'; Version='{reference.Version}'; Destination='{assemblyPath}'.");
EnsureInstalled(document, reference, cancellationToken);
}
if (!File.Exists(assemblyPath))
{
throw new InvalidOperationException(
@@ -115,10 +129,12 @@ internal sealed class PluginSharedContractManager : IDisposable
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
var temporaryPath = destinationPath + ".download";
var resolvedSource = entry.DownloadUrl;
try
{
if (AirAppMarketDefaults.TryResolveWorkspaceFile(entry.DownloadUrl, out var localSourcePath))
{
resolvedSource = localSourcePath;
File.Copy(localSourcePath, temporaryPath, overwrite: true);
}
else
@@ -136,6 +152,9 @@ internal sealed class PluginSharedContractManager : IDisposable
ValidateInstalledFile(temporaryPath, entry);
File.Move(temporaryPath, destinationPath, overwrite: true);
AppLogger.Info(
"PluginSharedContracts",
$"Installed shared contract. ContractId='{reference.Id}'; Version='{reference.Version}'; Source='{resolvedSource}'; Destination='{destinationPath}'.");
}
finally
{
@@ -145,6 +164,7 @@ internal sealed class PluginSharedContractManager : IDisposable
private AirAppMarketIndexDocument LoadIndex(CancellationToken cancellationToken)
{
AppLogger.Info("PluginSharedContracts", "Loading market index for shared contract resolution.");
var result = _indexService.LoadAsync(cancellationToken).GetAwaiter().GetResult();
if (!result.Success || result.Document is null)
{
@@ -152,6 +172,10 @@ internal sealed class PluginSharedContractManager : IDisposable
$"Failed to load market index for shared contract resolution: {result.ErrorMessage ?? "Unknown error"}");
}
AppLogger.Info(
"PluginSharedContracts",
$"Market index ready. Source='{result.Source}'; Location='{result.SourceLocation}'; Warning='{result.WarningMessage ?? string.Empty}'.");
return result.Document;
}

View File

@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
using FluentIcons.Common;
using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
@@ -17,11 +17,12 @@ public partial class SettingsWindow
private void InitializePluginSettingsNavigation()
{
if (_pluginSettingsPageHosts.Count > 0)
{
return;
}
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
}
private void RegisterPluginSettingsDefinitions()
{
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
@@ -31,7 +32,6 @@ public partial class SettingsWindow
if (contributions is not { Length: > 0 })
{
SettingsPluginNavSection.IsVisible = false;
return;
}
@@ -39,23 +39,21 @@ public partial class SettingsWindow
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
foreach (var contribution in contributions)
for (var i = 0; i < contributions.Length; i++)
{
var contribution = contributions[i];
var tag = BuildPluginSettingsTag(contribution);
var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
var navItem = CreateSettingsNavItem(tag, Symbol.PuzzlePiece, navigationTitle);
ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
_pluginSettingsPageHosts[tag] = CreatePluginSettingsPageHost(contribution);
SettingsPluginNavHost.Children.Add(navItem);
_pluginSettingsNavItems[tag] = navItem;
var pageHost = CreatePluginSettingsPageHost(contribution);
pageHost.IsVisible = false;
SettingsContentPagesHost.Children.Add(pageHost);
_pluginSettingsPageHosts[tag] = pageHost;
RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
tag,
BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId),
BuildPluginSettingsPageDescription(contribution),
FluentIcons.Common.Symbol.PuzzlePiece,
IndependentSettingsPageCategory.External,
200 + i,
$"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"));
}
SettingsPluginNavSection.IsVisible = SettingsPluginNavHost.Children.Count > 0;
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
@@ -72,6 +70,15 @@ public partial class SettingsWindow
: contribution.Plugin.Manifest.Name;
}
private string BuildPluginSettingsPageDescription(PluginSettingsPageContribution contribution)
{
return Lf(
"settings.page_desc.plugin_contributed_format",
"Settings page '{0}' is provided by plugin '{1}'.",
contribution.Registration.Title,
contribution.Plugin.Manifest.Name);
}
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
@@ -87,6 +94,7 @@ public partial class SettingsWindow
return new StackPanel
{
Spacing = 16,
MaxWidth = 920,
Children =
{
new TextBlock
@@ -94,12 +102,12 @@ public partial class SettingsWindow
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
Foreground = GetThemeBrush("TextFillColorPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
Foreground = GetThemeBrush("TextFillColorSecondaryBrush")
},
content
}
@@ -123,58 +131,32 @@ public partial class SettingsWindow
};
}
private void UpdatePluginSettingsPageVisibility(string? selectedTag)
{
foreach (var pair in _pluginSettingsPageHosts)
{
pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
}
}
internal void RefreshPluginSettingsNavigation()
{
foreach (var pair in _pluginSettingsPageHosts.ToArray())
{
if (_pluginSettingsNavItems.TryGetValue(pair.Key, out var navItem))
{
SettingsPluginNavHost.Children.Remove(navItem);
}
SettingsContentPagesHost.Children.Remove(pair.Value);
}
_pluginSettingsPageHosts.Clear();
_pluginSettingsNavItems.Clear();
SettingsPluginNavSection.IsVisible = false;
InitializePluginSettingsNavigation();
if (GetSettingsNavItem(_selectedSettingsTabTag) is null)
{
SelectSettingsTab("Plugins", persistSelection: false);
}
else
{
SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
}
var preferredTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
InitializeSettingsNavigation();
SelectSettingsTab(
_settingsPageDefinitions.ContainsKey(preferredTag) ? preferredTag : "Plugins",
persistSelection: false);
PluginSettingsPanel?.RefreshFromRuntime();
}
private string? GetSelectedSettingsTabTag()
{
return _selectedSettingsTabTag;
return NormalizeSettingsPageTag(_selectedSettingsTabTag);
}
private int ResolveSelectedSettingsTabIndex()
{
var selectedTag = GetSelectedSettingsTabTag();
if (string.IsNullOrWhiteSpace(selectedTag))
if (SettingsNavView?.MenuItems is null)
{
return 0;
}
var buttons = EnumerateSettingsNavItems().ToList();
for (var i = 0; i < buttons.Count; i++)
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
for (var i = 0; i < items.Count; i++)
{
if (string.Equals(buttons[i].Tag?.ToString(), selectedTag, StringComparison.OrdinalIgnoreCase))
if (string.Equals(items[i].Tag?.ToString(), NormalizeSettingsPageTag(_selectedSettingsTabTag), StringComparison.OrdinalIgnoreCase))
{
return i;
}
@@ -185,21 +167,32 @@ public partial class SettingsWindow
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
var buttons = EnumerateSettingsNavItems().ToList();
if (buttons.Count == 0)
if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag) &&
GetSettingsNavItem(snapshot.SettingsTabTag) is not null)
var items = SettingsNavView.MenuItems.OfType<NavigationViewItem>().ToList();
if (items.Count == 0)
{
SelectSettingsTab(snapshot.SettingsTabTag, persistSelection: false);
return;
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, buttons.Count - 1));
var button = buttons[safeIndex];
SelectSettingsTab(button.Tag?.ToString() ?? "Wallpaper", persistSelection: false);
if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
{
var normalizedTag = NormalizeSettingsPageTag(snapshot.SettingsTabTag);
var taggedItem = items
.FirstOrDefault(item => string.Equals(item.Tag?.ToString(), normalizedTag, StringComparison.OrdinalIgnoreCase));
if (taggedItem is not null)
{
_selectedSettingsTabTag = normalizedTag;
SettingsNavView.SelectedItem = taggedItem;
return;
}
}
var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, items.Count - 1));
_selectedSettingsTabTag = items[safeIndex].Tag?.ToString() ?? _selectedSettingsTabTag;
SettingsNavView.SelectedItem = items[safeIndex];
}
}