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> <OutputPath>bin\$(Configuration)\$(TargetFramework)\content\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath> <AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath> <AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
<PluginPackageOutputDirectory>..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\</PluginPackageOutputDirectory> <PluginPackageOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Packages\</PluginPackageOutputDirectory>
<PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).laapp</PluginPackagePath> <PluginPackagePath>$(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp</PluginPackagePath>
<LegacyLoosePluginOutputDirectory>..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\SamplePlugin\</LegacyLoosePluginOutputDirectory> <LegacyLoosePluginOutputDirectory>$(MSBuildThisFileDirectory)artifacts\Loose\</LegacyLoosePluginOutputDirectory>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,32 +1,50 @@
using System;
using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using System;
using System.Linq;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views; using LanMountainDesktop.Views;
using AvaloniaWebView;
using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop; namespace LanMountainDesktop;
public partial class App : Application 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 AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService(); private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private bool _exitCleanupCompleted; private bool _exitCleanupCompleted;
private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
private ShutdownIntent _shutdownIntent;
private SettingsWindow? _traySettingsWindow; private readonly IndependentSettingsModuleService _independentSettingsModuleService = new();
private TrayIcons? _trayIcons; private TrayIcons? _trayIcons;
private PluginRuntimeService? _pluginRuntimeService; private PluginRuntimeService? _pluginRuntimeService;
private MainWindow? _mainWindow;
private bool _mainWindowClosed;
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; } internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle => internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -35,6 +53,11 @@ public partial class App : Application
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService; public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle; public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
{
_independentSettingsModuleService.ShowOrActivate(source, pageTag);
}
public override void Initialize() public override void Initialize()
{ {
AppLogger.Info("App", "Initializing application resources."); AppLogger.Info("App", "Initializing application resources.");
@@ -62,12 +85,8 @@ public partial class App : Application
AppLogger.Info("App", "Desktop lifetime exit triggered."); AppLogger.Info("App", "Desktop lifetime exit triggered.");
PerformExitCleanup(); PerformExitCleanup();
}; };
desktop.MainWindow = new MainWindow
{ CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
DataContext = new MainWindowViewModel(),
};
AppLogger.Info("App", $"Main window created. LogFile={AppLogger.LogFilePath}");
LogBrowserStartupDiagnostics();
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow); CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
} }
@@ -81,42 +100,14 @@ public partial class App : Application
Reason: "User selected Exit App from the tray menu.")); 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) private void OnTraySettingsClick(object? sender, EventArgs e)
{ {
if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime) OpenIndependentSettingsModule("TrayMenu");
{
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);
} }
private void OnTrayRestartClick(object? sender, EventArgs e) private void OnTrayRestartClick(object? sender, EventArgs e)
@@ -209,19 +200,25 @@ public partial class App : Application
{ {
var menu = new NativeMenu(); 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; settingsItem.Click += OnTraySettingsClick;
menu.Items.Add(settingsItem); menu.Items.Add(settingsItem);
menu.Items.Add(new NativeMenuItemSeparator()); 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; restartItem.Click += OnTrayRestartClick;
menu.Items.Add(restartItem); menu.Items.Add(restartItem);
menu.Items.Add(new NativeMenuItemSeparator()); 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; exitItem.Click += OnTrayExitClick;
menu.Items.Add(exitItem); menu.Items.Add(exitItem);
@@ -245,6 +242,11 @@ public partial class App : Application
} }
private void ActivateMainWindow() private void ActivateMainWindow()
{
RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance");
}
private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
@@ -253,13 +255,11 @@ public partial class App : Application
return; return;
} }
if (desktop.MainWindow is not Window mainWindow)
{
return;
}
try try
{ {
var mainWindow = GetOrCreateMainWindow(desktop, source);
mainWindow.ShowInTaskbar = true;
if (!mainWindow.IsVisible) if (!mainWindow.IsVisible)
{ {
mainWindow.Show(); mainWindow.Show();
@@ -278,18 +278,68 @@ public partial class App : Application
mainWindow.Activate(); mainWindow.Activate();
mainWindow.Topmost = true; mainWindow.Topmost = true;
mainWindow.Topmost = false; 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) 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); }, 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 _) private void OnAppSettingsSaved(string _)
{ {
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
@@ -311,18 +361,7 @@ public partial class App : Application
_exitCleanupCompleted = true; _exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved; AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
try _independentSettingsModuleService.CloseIfOpen();
{
_traySettingsWindow?.Close();
}
catch (Exception ex)
{
AppLogger.Warn("App", "Failed to close tray-opened settings window during shutdown.", ex);
}
finally
{
_traySettingsWindow = null;
}
try try
{ {
@@ -342,6 +381,171 @@ public partial class App : Application
DisposeTrayIcon(); 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() private void LogBrowserStartupDiagnostics()
{ {
try try

View File

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

View File

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

View File

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

View File

@@ -80,11 +80,13 @@ public partial class MainWindow
if (_isSettingsOpen) if (_isSettingsOpen)
{ {
CloseSettingsPage(); CloseSettingsPage(immediate: true);
return;
} }
OpenSettingsPage(); if (Application.Current is App app)
{
app.OpenIndependentSettingsModule("MainWindow");
}
} }
private void OnCloseSettingsClick(object? sender, RoutedEventArgs e) 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -8,16 +8,16 @@
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"> x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage">
<StackPanel x:Name="AboutSettingsPanel" Spacing="20"> <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"> <Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" CornerRadius="{DynamicResource DesignCornerRadiusMd}" Padding="20">
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> <TextBlock Text="LanMountainDesktop" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> <TextBlock Text="Modern desktop shell experience." FontSize="13" Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<Separator Background="{DynamicResource AdaptiveButtonBorderBrush}" Margin="0,8" /> <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="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> </StackPanel>
</Border> </Border>
@@ -49,17 +49,17 @@
Text="Current actual backend" Text="Current actual backend"
FontSize="12" FontSize="12"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendValueTextBlock" <TextBlock x:Name="CurrentRenderBackendValueTextBlock"
Text="Current backend: Software" Text="Current backend: Software"
FontSize="13" FontSize="13"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="CurrentRenderBackendImplementationTextBlock" <TextBlock x:Name="CurrentRenderBackendImplementationTextBlock"
Text="Runtime implementation is unavailable." Text="Runtime implementation is unavailable."
FontSize="12" FontSize="12"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel> </StackPanel>
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ComboBox x:Name="AppRenderModeComboBox" <ComboBox x:Name="AppRenderModeComboBox"
@@ -77,3 +77,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -8,19 +8,27 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.ColorSettingsPage"> x:Class="LanMountainDesktop.Views.SettingsPages.ColorSettingsPage">
<StackPanel x:Name="ColorSettingsPanel" <StackPanel x:Name="ColorSettingsPanel"
MaxWidth="920"
Spacing="16"> Spacing="16">
<TextBlock x:Name="ColorPanelTitleTextBlock" <TextBlock x:Name="ColorPanelTitleTextBlock"
IsVisible="False"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Color" /> 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"> <Border Classes="settings-expander-shell">
<ui:SettingsExpander x:Name="ThemeModeSettingsExpander" <ui:SettingsExpander x:Name="ThemeModeSettingsExpander"
Header="&#26085;&#22812;&#27169;&#24335;" Header="&#26085;&#22812;&#27169;&#24335;"
Description="&#20999;&#25442;&#24212;&#29992;&#30340;&#27973;&#33394;&#25110;&#28145;&#33394;&#20027;&#39064;&#12290;"> Description="&#20999;&#25442;&#24212;&#29992;&#30340;&#27973;&#33394;&#25110;&#28145;&#33394;&#20027;&#39064;&#12290;">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="DarkTheme"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ToggleSwitch x:Name="NightModeToggleSwitch" <ToggleSwitch x:Name="NightModeToggleSwitch"
@@ -35,14 +43,15 @@
Header="&#20027;&#39064;&#33394;" Header="&#20027;&#39064;&#33394;"
Description="&#36873;&#25321;&#24212;&#29992;&#30340;&#20027;&#39064;&#28857;&#32512;&#33394;&#12290;"> Description="&#36873;&#25321;&#24212;&#29992;&#30340;&#20027;&#39064;&#28857;&#32512;&#33394;&#12290;">
<ui:SettingsExpander.IconSource> <ui:SettingsExpander.IconSource>
<ic:SymbolIconSource Symbol="Color"
IconVariant="Regular" />
</ui:SettingsExpander.IconSource> </ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem> <ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<TextBlock x:Name="RecommendedColorsLabelTextBlock" <TextBlock x:Name="RecommendedColorsLabelTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Recommended Colors" /> Text="Recommended Colors" />
<WrapPanel ItemWidth="72" <WrapPanel ItemWidth="72"
ItemHeight="56" ItemHeight="56"
@@ -119,7 +128,7 @@
ColumnSpacing="10"> ColumnSpacing="10">
<TextBlock x:Name="SystemMonetColorsLabelTextBlock" <TextBlock x:Name="SystemMonetColorsLabelTextBlock"
VerticalAlignment="Center" VerticalAlignment="Center"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="System Monet Colors" /> Text="System Monet Colors" />
<Button x:Name="RefreshMonetColorsButton" <Button x:Name="RefreshMonetColorsButton"
Grid.Column="1" Grid.Column="1"
@@ -197,8 +206,13 @@
</ui:SettingsExpander> </ui:SettingsExpander>
</Border> </Border>
<Border Classes="settings-expander-shell"
Padding="16,14">
<TextBlock x:Name="ThemeColorStatusTextBlock" <TextBlock x:Name="ThemeColorStatusTextBlock"
Foreground="{DynamicResource AdaptiveTextMutedBrush}" Foreground="{DynamicResource AdaptiveTextMutedBrush}"
TextWrapping="Wrap"
Text="Theme color is ready." /> Text="Theme color is ready." />
</Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,30 +9,32 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.GridSettingsPage"> x:Class="LanMountainDesktop.Views.SettingsPages.GridSettingsPage">
<Grid x:Name="GridSettingsPanel" <Grid x:Name="GridSettingsPanel"
ColumnDefinitions="*, *" ColumnDefinitions="280, *"
RowDefinitions="Auto, *"> RowDefinitions="Auto, *">
<TextBlock x:Name="GridPanelTitleTextBlock" <TextBlock x:Name="GridPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,24" Margin="0,0,0,20"
Text="调整网格布局" /> Text="璋冩暣缃戞牸甯冨眬" />
<!-- Left Column: Grid Preview --> <!-- Left Column: Grid Preview -->
<Border x:Name="GridPreviewHost" <Border x:Name="GridPreviewHost"
Grid.Row="1" Grid.Column="0" Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0" Margin="0,0,20,0"
Width="256"
MaxWidth="256"
VerticalAlignment="Top" VerticalAlignment="Top"
HorizontalAlignment="Stretch"> HorizontalAlignment="Left">
<Border x:Name="GridPreviewFrame" <Border x:Name="GridPreviewFrame"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="28" CornerRadius="22"
Background="#FF1A1A1A" Background="#FF1A1A1A"
Padding="12"> Padding="8">
<Border x:Name="GridPreviewViewport" <Border x:Name="GridPreviewViewport"
ClipToBounds="True" ClipToBounds="True"
CornerRadius="16" CornerRadius="14"
Background="#30111827"> Background="#30111827">
<Panel> <Panel>
<Canvas x:Name="GridPreviewLinesCanvas" <Canvas x:Name="GridPreviewLinesCanvas"
@@ -63,7 +65,7 @@
<Border x:Name="GridPreviewTaskbarFixedActionsHost" Grid.Column="0"> <Border x:Name="GridPreviewTaskbarFixedActionsHost" Grid.Column="0">
<StackPanel x:Name="GridPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3"> <StackPanel x:Name="GridPreviewBackButtonVisual" Orientation="Horizontal" Spacing="3">
<fi:SymbolIcon Classes="icon-s" Symbol="Window" /> <fi:SymbolIcon Classes="icon-s" Symbol="Window" />
<TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="回到Windows" VerticalAlignment="Center" /> <TextBlock x:Name="GridPreviewBackButtonTextBlock" Text="鍥炲埌Windows" VerticalAlignment="Center" />
</StackPanel> </StackPanel>
</Border> </Border>
<StackPanel x:Name="GridPreviewTaskbarDynamicActionsHost" <StackPanel x:Name="GridPreviewTaskbarDynamicActionsHost"
@@ -90,7 +92,7 @@
<!-- Right Column: Settings Content --> <!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1" <StackPanel Grid.Row="1" Grid.Column="1"
Margin="16,0,0,0" Margin="20,0,0,0"
Spacing="16"> Spacing="16">
<Border Classes="settings-expander-shell"> <Border Classes="settings-expander-shell">
@@ -109,7 +111,7 @@
Width="80" Width="80"
Minimum="6" Minimum="6"
Maximum="96" Maximum="96"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="12" /> Value="12" />
</Grid> </Grid>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
@@ -144,14 +146,14 @@
Width="80" Width="80"
Minimum="0" Minimum="0"
Maximum="30" Maximum="30"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="18" /> Value="18" />
</Grid> </Grid>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem> <ui:SettingsExpanderItem>
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="GridEdgeInsetComputedPxTextBlock" <TextBlock x:Name="GridEdgeInsetComputedPxTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" /> Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer> </ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
@@ -161,12 +163,13 @@
<Button x:Name="ApplyGridButton" <Button x:Name="ApplyGridButton"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
Padding="0,10" Padding="0,10"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Content="应用" /> Content="搴旂敤" />
<TextBlock x:Name="GridInfoTextBlock" <TextBlock x:Name="GridInfoTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Grid: - cols x - rows (1:1)" /> Text="Grid: - cols x - rows (1:1)" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<TextBlock x:Name="LauncherSettingsPanelTitleTextBlock" <TextBlock x:Name="LauncherSettingsPanelTitleTextBlock"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="App Launcher" /> Text="App Launcher" />
<Border Classes="settings-expander-shell"> <Border Classes="settings-expander-shell">
@@ -25,11 +25,11 @@
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<StackPanel Spacing="10"> <StackPanel Spacing="10">
<TextBlock x:Name="LauncherHiddenItemsDescriptionTextBlock" <TextBlock x:Name="LauncherHiddenItemsDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Right-click an icon in launcher to hide it. Hidden entries appear here." /> Text="Right-click an icon in launcher to hide it. Hidden entries appear here." />
<TextBlock x:Name="LauncherHiddenItemsEmptyTextBlock" <TextBlock x:Name="LauncherHiddenItemsEmptyTextBlock"
IsVisible="False" IsVisible="False"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="No hidden items." /> Text="No hidden items." />
</StackPanel> </StackPanel>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
@@ -37,3 +37,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,7 +12,7 @@
<TextBlock x:Name="RegionPanelTitleTextBlock" <TextBlock x:Name="RegionPanelTitleTextBlock"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Region" /> Text="Region" />
<Border Classes="settings-expander-shell"> <Border Classes="settings-expander-shell">
@@ -25,7 +25,7 @@
<ui:SettingsExpander.Footer> <ui:SettingsExpander.Footer>
<ComboBox x:Name="LanguageComboBox" <ComboBox x:Name="LanguageComboBox"
Width="220"> 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" /> <ComboBoxItem x:Name="LanguageEnglishItem" Tag="en-US" Content="English" />
</ComboBox> </ComboBox>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
@@ -47,3 +47,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<TextBlock x:Name="StatusBarPanelTitleTextBlock" <TextBlock x:Name="StatusBarPanelTitleTextBlock"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Status Bar" /> Text="Status Bar" />
<Border Classes="settings-expander-shell"> <Border Classes="settings-expander-shell">
@@ -80,7 +80,7 @@
<ui:SettingsExpanderItem.Footer> <ui:SettingsExpanderItem.Footer>
<TextBlock x:Name="StatusBarSpacingComputedPxTextBlock" <TextBlock x:Name="StatusBarSpacingComputedPxTextBlock"
HorizontalAlignment="Right" HorizontalAlignment="Right"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text=">= 0 px" /> Text=">= 0 px" />
</ui:SettingsExpanderItem.Footer> </ui:SettingsExpanderItem.Footer>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
@@ -88,3 +88,4 @@
</Border> </Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -12,7 +12,7 @@
<TextBlock x:Name="UpdatePanelTitleTextBlock" <TextBlock x:Name="UpdatePanelTitleTextBlock"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Update" /> Text="Update" />
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}" <Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
@@ -22,31 +22,31 @@
<TextBlock x:Name="UpdateCurrentVersionLabelTextBlock" <TextBlock x:Name="UpdateCurrentVersionLabelTextBlock"
Text="Current Version" Text="Current Version"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateCurrentVersionValueTextBlock" <TextBlock x:Name="UpdateCurrentVersionValueTextBlock"
Grid.Column="1" Grid.Column="1"
Text="-" Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionLabelTextBlock" <TextBlock x:Name="UpdateLatestVersionLabelTextBlock"
Grid.Row="1" Grid.Row="1"
Text="Latest Release" Text="Latest Release"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateLatestVersionValueTextBlock" <TextBlock x:Name="UpdateLatestVersionValueTextBlock"
Grid.Row="1" Grid.Column="1" Grid.Row="1" Grid.Column="1"
Text="-" Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtLabelTextBlock" <TextBlock x:Name="UpdatePublishedAtLabelTextBlock"
Grid.Row="2" Grid.Row="2"
Text="Published At" Text="Published At"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdatePublishedAtValueTextBlock" <TextBlock x:Name="UpdatePublishedAtValueTextBlock"
Grid.Row="2" Grid.Column="1" Grid.Row="2" Grid.Column="1"
Text="-" Text="-"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</Grid> </Grid>
</Border> </Border>
@@ -65,7 +65,7 @@
<TextBlock x:Name="UpdateChannelLabelTextBlock" <TextBlock x:Name="UpdateChannelLabelTextBlock"
Text="Update Channel" Text="Update Channel"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<ListBox x:Name="UpdateChannelChipListBox" <ListBox x:Name="UpdateChannelChipListBox"
Classes="settings-chip-list"> Classes="settings-chip-list">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
@@ -110,13 +110,14 @@
IsVisible="False" /> IsVisible="False" />
<TextBlock x:Name="UpdateDownloadProgressTextBlock" <TextBlock x:Name="UpdateDownloadProgressTextBlock"
Text="Download progress: -" Text="Download progress: -"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
<TextBlock x:Name="UpdateStatusTextBlock" <TextBlock x:Name="UpdateStatusTextBlock"
Text="Ready to check for updates." Text="Ready to check for updates."
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" /> Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
</StackPanel> </StackPanel>
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
</Border> </Border>
</StackPanel> </StackPanel>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -9,31 +9,33 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage"> x:Class="LanMountainDesktop.Views.SettingsPages.WallpaperSettingsPage">
<Grid x:Name="WallpaperSettingsPanel" <Grid x:Name="WallpaperSettingsPanel"
ColumnDefinitions="*, *" ColumnDefinitions="280, *"
RowDefinitions="Auto, *"> RowDefinitions="Auto, *">
<TextBlock x:Name="WallpaperPanelTitleTextBlock" <TextBlock x:Name="WallpaperPanelTitleTextBlock"
Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2"
FontSize="28" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Margin="0,0,0,24" Margin="0,0,0,20"
Text="Personalize Wallpaper" /> Text="Personalize Wallpaper" />
<!-- Left Column: Monitor Preview --> <!-- Left Column: Monitor Preview -->
<Border x:Name="WallpaperPreviewHost" <Border x:Name="WallpaperPreviewHost"
Grid.Row="1" Grid.Column="0" Grid.Row="1" Grid.Column="0"
Margin="0,0,16,0" Margin="0,0,20,0"
VerticalAlignment="Stretch" Width="256"
HorizontalAlignment="Stretch"> MaxWidth="256"
VerticalAlignment="Top"
HorizontalAlignment="Left">
<!-- Monitor Frame (Bezel) --> <!-- Monitor Frame (Bezel) -->
<Border x:Name="WallpaperPreviewFrame" <Border x:Name="WallpaperPreviewFrame"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
CornerRadius="28" CornerRadius="22"
Background="#FF1A1A1A" Background="#FF1A1A1A"
Padding="12"> Padding="8">
<Border x:Name="WallpaperPreviewViewport" <Border x:Name="WallpaperPreviewViewport"
ClipToBounds="True" ClipToBounds="True"
CornerRadius="12" CornerRadius="14"
Background="#30111827"> Background="#30111827">
<Grid> <Grid>
<Image x:Name="WallpaperPreviewVideoImage" <Image x:Name="WallpaperPreviewVideoImage"
@@ -96,14 +98,14 @@
<!-- Right Column: Settings Content --> <!-- Right Column: Settings Content -->
<StackPanel Grid.Row="1" Grid.Column="1" <StackPanel Grid.Row="1" Grid.Column="1"
Margin="16,0,0,0" Margin="20,0,0,0"
Spacing="16"> Spacing="16">
<StackPanel Spacing="8"> <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" <TextBlock x:Name="WallpaperPathTextBlock"
FontSize="14" FontSize="14"
FontWeight="Medium" FontWeight="Medium"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Text="No file selected" /> Text="No file selected" />
<TextBlock x:Name="WallpaperStatusTextBlock" <TextBlock x:Name="WallpaperStatusTextBlock"
@@ -114,7 +116,7 @@
<Separator Background="{DynamicResource SurfaceStrokeColorDefaultBrush}" Height="1" Margin="0,8" /> <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"> <Grid ColumnDefinitions="*, *" ColumnSpacing="12">
<Button x:Name="PickWallpaperButton" <Button x:Name="PickWallpaperButton"
@@ -149,3 +151,4 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
</UserControl> </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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
@@ -11,7 +11,7 @@
<Style Selector="StackPanel.weather-settings-root TextBlock.section-eyebrow"> <Style Selector="StackPanel.weather-settings-root TextBlock.section-eyebrow">
<Setter Property="FontSize" Value="13" /> <Setter Property="FontSize" Value="13" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" /> <Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
</Style> </Style>
<Style Selector="StackPanel.weather-settings-root Border.preview-icon-shell"> <Style Selector="StackPanel.weather-settings-root Border.preview-icon-shell">
@@ -42,7 +42,7 @@
<TextBlock x:Name="WeatherPanelTitleTextBlock" <TextBlock x:Name="WeatherPanelTitleTextBlock"
FontSize="28" FontSize="28"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Weather" /> Text="Weather" />
<StackPanel Spacing="8"> <StackPanel Spacing="8">
@@ -70,7 +70,7 @@
Text="--" /> Text="--" />
<TextBlock x:Name="WeatherPreviewUpdatedTextBlock" <TextBlock x:Name="WeatherPreviewUpdatedTextBlock"
FontSize="13" FontSize="13"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="-" /> Text="-" />
</StackPanel> </StackPanel>
@@ -92,7 +92,7 @@
<TextBlock x:Name="WeatherPreviewResultTextBlock" <TextBlock x:Name="WeatherPreviewResultTextBlock"
Grid.Row="1" Grid.Row="1"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Use refresh to verify your weather configuration." /> Text="Use refresh to verify your weather configuration." />
</Grid> </Grid>
</Border> </Border>
@@ -138,7 +138,7 @@
FontWeight="SemiBold" FontWeight="SemiBold"
Text="City Selection" /> Text="City Selection" />
<TextBlock x:Name="WeatherLocationSelectionDescriptionTextBlock" <TextBlock x:Name="WeatherLocationSelectionDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="Select the current city used for weather queries." /> Text="Select the current city used for weather queries." />
</StackPanel> </StackPanel>
@@ -157,7 +157,7 @@
FontSize="12" FontSize="12"
TextAlignment="Right" TextAlignment="Right"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" /> Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel> </StackPanel>
</Grid> </Grid>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
@@ -216,7 +216,7 @@
<TextBlock x:Name="WeatherSearchStatusTextBlock" <TextBlock x:Name="WeatherSearchStatusTextBlock"
FontSize="12" FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="Search by city name and apply one location." /> Text="Search by city name and apply one location." />
</StackPanel> </StackPanel>
@@ -266,7 +266,7 @@
Watermark="Display name (optional)" /> Watermark="Display name (optional)" />
<TextBlock x:Name="WeatherCoordinateStatusTextBlock" <TextBlock x:Name="WeatherCoordinateStatusTextBlock"
FontSize="12" FontSize="12"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
</ui:SettingsExpanderItem> </ui:SettingsExpanderItem>
@@ -288,7 +288,7 @@
FontWeight="SemiBold" FontWeight="SemiBold"
Text="Exclude List" /> Text="Exclude List" />
<TextBlock x:Name="WeatherAlertListDescriptionTextBlock" <TextBlock x:Name="WeatherAlertListDescriptionTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="One exclusion rule per line." /> Text="One exclusion rule per line." />
</StackPanel> </StackPanel>
@@ -321,7 +321,7 @@
<Border Classes="settings-note-shell"> <Border Classes="settings-note-shell">
<TextBlock x:Name="WeatherFooterHintTextBlock" <TextBlock x:Name="WeatherFooterHintTextBlock"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
Text="Desktop weather widgets will reuse the location and alert exclusion settings configured here." /> Text="Desktop weather widgets will reuse the location and alert exclusion settings configured here." />
</Border> </Border>
@@ -352,3 +352,4 @@
</Grid> </Grid>
</StackPanel> </StackPanel>
</UserControl> </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;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
public partial class SettingsWindow public partial class SettingsWindow
{ {
// --- WallpaperSettingsPage --- private string CurrentControlAccessStage => _controlsBound ? "module init" : "control binding";
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")!;
// --- GridSettingsPage --- private T? TryGetOptionalPageControl<T>(Control? pageRoot, string controlName)
internal TextBlock GridPanelTitleTextBlock => GridSettingsPanel.FindControl<TextBlock>("GridPanelTitleTextBlock")!; where T : Control
internal Border GridPreviewHost => GridSettingsPanel.FindControl<Border>("GridPreviewHost")!; {
internal Border GridPreviewFrame => GridSettingsPanel.FindControl<Border>("GridPreviewFrame")!; return pageRoot?.FindControl<T>(controlName);
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")!;
// --- ColorSettingsPage --- private T RequirePageControl<T>(Control pageRoot, string controlName)
internal TextBlock ColorPanelTitleTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ColorPanelTitleTextBlock")!; where T : Control
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")!; var control = pageRoot.FindControl<T>(controlName);
internal ToggleSwitch NightModeToggleSwitch => ColorSettingsPanel.FindControl<ToggleSwitch>("NightModeToggleSwitch")!; if (control is null)
internal TextBlock ThemeColorStatusTextBlock => ColorSettingsPanel.FindControl<TextBlock>("ThemeColorStatusTextBlock")!; {
internal TextBlock RecommendedColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("RecommendedColorsLabelTextBlock")!; throw new InvalidOperationException(
internal TextBlock SystemMonetColorsLabelTextBlock => ColorSettingsPanel.FindControl<TextBlock>("SystemMonetColorsLabelTextBlock")!; $"Independent settings module control resolution failed. Page='{pageRoot.Name ?? pageRoot.GetType().Name}'; Control='{controlName}'; Stage='{CurrentControlAccessStage}'.");
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")!;
// --- StatusBarSettingsPage --- return control;
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")!;
// --- RegionSettingsPage --- private T RequireSettingsPage<T>(T? page, string pageName)
internal TextBlock RegionPanelTitleTextBlock => RegionSettingsPanel.FindControl<TextBlock>("RegionPanelTitleTextBlock")!; where T : Control
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")!; return page ?? throw new InvalidOperationException(
internal ComboBox LanguageComboBox => RegionSettingsPanel.FindControl<ComboBox>("LanguageComboBox")!; $"Independent settings module page resolution failed. Page='{pageName}'; Stage='{CurrentControlAccessStage}'.");
internal ComboBoxItem LanguageChineseItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageChineseItem")!; }
internal ComboBoxItem LanguageEnglishItem => RegionSettingsPanel.FindControl<ComboBoxItem>("LanguageEnglishItem")!;
internal ComboBox TimeZoneComboBox => RegionSettingsPanel.FindControl<ComboBox>("TimeZoneComboBox")!;
// --- WeatherSettingsPage --- private WallpaperSettingsPage WallpaperSettingsPageRoot => RequireSettingsPage(WallpaperSettingsPanel, nameof(WallpaperSettingsPanel));
internal TextBlock WeatherPanelTitleTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPanelTitleTextBlock")!; private GridSettingsPage GridSettingsPageRoot => RequireSettingsPage(GridSettingsPanel, nameof(GridSettingsPanel));
internal TextBlock WeatherPreviewSectionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherPreviewSectionTextBlock")!; private ColorSettingsPage ColorSettingsPageRoot => RequireSettingsPage(ColorSettingsPanel, nameof(ColorSettingsPanel));
internal TextBlock WeatherSettingsSectionTextBlock => WeatherSettingsPanel.FindControl<TextBlock>("WeatherSettingsSectionTextBlock")!; private StatusBarSettingsPage StatusBarSettingsPageRoot => RequireSettingsPage(StatusBarSettingsPanel, nameof(StatusBarSettingsPanel));
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherPreviewSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherPreviewSettingsExpander")!; private RegionSettingsPage RegionSettingsPageRoot => RequireSettingsPage(RegionSettingsPanel, nameof(RegionSettingsPanel));
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherLocationSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherLocationSettingsExpander")!; private WeatherSettingsPage WeatherSettingsPageRoot => RequireSettingsPage(WeatherSettingsPanel, nameof(WeatherSettingsPanel));
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCitySearchSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCitySearchSettingsExpander")!; private UpdateSettingsPage UpdateSettingsPageRoot => RequireSettingsPage(UpdateSettingsPanel, nameof(UpdateSettingsPanel));
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherCoordinateSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherCoordinateSettingsExpander")!; private AboutSettingsPage AboutSettingsPageRoot => RequireSettingsPage(AboutSettingsPanel, nameof(AboutSettingsPanel));
internal FluentAvalonia.UI.Controls.SettingsExpander WeatherAlertFilterSettingsExpander => WeatherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("WeatherAlertFilterSettingsExpander")!; private LauncherSettingsPage LauncherSettingsPageRoot => RequireSettingsPage(LauncherSettingsPanel, nameof(LauncherSettingsPanel));
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")!;
// --- UpdateSettingsPage --- private T WallpaperControl<T>(string name) where T : Control => RequirePageControl<T>(WallpaperSettingsPageRoot, name);
internal TextBlock UpdatePanelTitleTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePanelTitleTextBlock")!; private T GridControl<T>(string name) where T : Control => RequirePageControl<T>(GridSettingsPageRoot, name);
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateOptionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateOptionsSettingsExpander")!; private T ColorControl<T>(string name) where T : Control => RequirePageControl<T>(ColorSettingsPageRoot, name);
internal FluentAvalonia.UI.Controls.SettingsExpander UpdateActionsSettingsExpander => UpdateSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("UpdateActionsSettingsExpander")!; private T StatusBarControl<T>(string name) where T : Control => RequirePageControl<T>(StatusBarSettingsPageRoot, name);
internal TextBlock UpdateCurrentVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionLabelTextBlock")!; private T RegionControl<T>(string name) where T : Control => RequirePageControl<T>(RegionSettingsPageRoot, name);
internal TextBlock UpdateCurrentVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateCurrentVersionValueTextBlock")!; private T WeatherControl<T>(string name) where T : Control => RequirePageControl<T>(WeatherSettingsPageRoot, name);
internal TextBlock UpdateLatestVersionLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionLabelTextBlock")!; private T UpdateControl<T>(string name) where T : Control => RequirePageControl<T>(UpdateSettingsPageRoot, name);
internal TextBlock UpdateLatestVersionValueTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdateLatestVersionValueTextBlock")!; private T AboutControl<T>(string name) where T : Control => RequirePageControl<T>(AboutSettingsPageRoot, name);
internal TextBlock UpdatePublishedAtLabelTextBlock => UpdateSettingsPanel.FindControl<TextBlock>("UpdatePublishedAtLabelTextBlock")!; private T LauncherControl<T>(string name) where T : Control => RequirePageControl<T>(LauncherSettingsPageRoot, name);
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")!;
// --- AboutSettingsPage --- internal TextBlock WallpaperPanelTitleTextBlock => WallpaperControl<TextBlock>("WallpaperPanelTitleTextBlock");
internal TextBlock AboutPanelTitleTextBlock => AboutSettingsPanel.FindControl<TextBlock>("AboutPanelTitleTextBlock")!; internal TextBlock WallpaperPathTextBlock => WallpaperControl<TextBlock>("WallpaperPathTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutStartupSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutStartupSettingsExpander")!; internal TextBlock WallpaperStatusTextBlock => WallpaperControl<TextBlock>("WallpaperStatusTextBlock");
internal FluentAvalonia.UI.Controls.SettingsExpander AboutRenderModeSettingsExpander => AboutSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("AboutRenderModeSettingsExpander")!; internal ComboBox WallpaperPlacementComboBox => WallpaperControl<ComboBox>("WallpaperPlacementComboBox");
internal ToggleSwitch AutoStartWithWindowsToggleSwitch => AboutSettingsPanel.FindControl<ToggleSwitch>("AutoStartWithWindowsToggleSwitch")!; internal Border WallpaperPreviewHost => WallpaperControl<Border>("WallpaperPreviewHost");
internal ComboBox AppRenderModeComboBox => AboutSettingsPanel.FindControl<ComboBox>("AppRenderModeComboBox")!; internal Border WallpaperPreviewFrame => WallpaperControl<Border>("WallpaperPreviewFrame");
internal TextBlock CurrentRenderBackendLabelTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendLabelTextBlock")!; internal Border WallpaperPreviewViewport => WallpaperControl<Border>("WallpaperPreviewViewport");
internal TextBlock CurrentRenderBackendValueTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendValueTextBlock")!; internal Image WallpaperPreviewVideoImage => WallpaperControl<Image>("WallpaperPreviewVideoImage");
internal TextBlock CurrentRenderBackendImplementationTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CurrentRenderBackendImplementationTextBlock")!; internal Grid WallpaperPreviewGrid => WallpaperControl<Grid>("WallpaperPreviewGrid");
internal TextBlock VersionTextBlock => AboutSettingsPanel.FindControl<TextBlock>("VersionTextBlock")!; internal Border WallpaperPreviewTopStatusBarHost => WallpaperControl<Border>("WallpaperPreviewTopStatusBarHost");
internal TextBlock CodeNameTextBlock => AboutSettingsPanel.FindControl<TextBlock>("CodeNameTextBlock")!; internal StackPanel WallpaperPreviewTopStatusComponentsPanel => WallpaperControl<StackPanel>("WallpaperPreviewTopStatusComponentsPanel");
internal TextBlock FontInfoTextBlock => AboutSettingsPanel.FindControl<TextBlock>("FontInfoTextBlock")!; 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 GridPanelTitleTextBlock => GridControl<TextBlock>("GridPanelTitleTextBlock");
internal TextBlock LauncherSettingsPanelTitleTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherSettingsPanelTitleTextBlock")!; internal Border GridPreviewHost => GridControl<Border>("GridPreviewHost");
internal FluentAvalonia.UI.Controls.SettingsExpander LauncherHiddenItemsSettingsExpander => LauncherSettingsPanel.FindControl<FluentAvalonia.UI.Controls.SettingsExpander>("LauncherHiddenItemsSettingsExpander")!; internal Border GridPreviewFrame => GridControl<Border>("GridPreviewFrame");
internal TextBlock LauncherHiddenItemsEmptyTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsEmptyTextBlock")!; internal Border GridPreviewViewport => GridControl<Border>("GridPreviewViewport");
internal TextBlock LauncherHiddenItemsDescriptionTextBlock => LauncherSettingsPanel.FindControl<TextBlock>("LauncherHiddenItemsDescriptionTextBlock")!; 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,18 +8,61 @@ using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls;
using FluentIcons.Avalonia.Fluent; using FluentIcons.Avalonia.Fluent;
using FluentIcons.Common; using FluentIcons.Common;
using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.ComponentSystem;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
using FluentIconVariant = FluentIcons.Common.IconVariant;
using FluentSymbol = FluentIcons.Common.Symbol;
using FluentSymbolIconSource = FluentIcons.Avalonia.Fluent.SymbolIconSource;
public partial class SettingsWindow public partial class SettingsWindow
{ {
private readonly Dictionary<string, Control> _builtInSettingsPageHosts = new(StringComparer.OrdinalIgnoreCase);
internal void Open(string? pageTag = null)
{
if (!string.IsNullOrWhiteSpace(pageTag))
{
_selectedSettingsTabTag = NormalizeSettingsPageTag(pageTag);
if (_independentModuleInitializationCompleted)
{
SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
}
}
if (!IsVisible)
{
Show();
}
if (WindowState == WindowState.Minimized)
{
WindowState = WindowState.Normal;
}
Activate();
}
internal void PrepareForForceClose()
{
_allowIndependentSettingsModuleRealClose = true;
}
protected override void OnClosed(EventArgs e) protected override void OnClosed(EventArgs e)
{
AppLogger.Info(
"IndependentSettingsModule",
$"PreviewCleanupStarted; Stage='WindowCloseCleanup'; Module='WallpaperPreview'; CloseRequested={_isIndependentSettingsModuleClosing}.");
try
{ {
_persistSettingsDebounceTimer?.Dispose(); _persistSettingsDebounceTimer?.Dispose();
_persistSettingsDebounceTimer = null; _persistSettingsDebounceTimer = null;
@@ -46,111 +89,251 @@ public partial class SettingsWindow
} }
_launcherIconCache.Clear(); _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; PendingRestartStateService.StateChanged -= OnPendingRestartStateChanged;
Closing -= OnIndependentSettingsModuleClosing;
base.OnClosed(e); base.OnClosed(e);
AppLogger.Info("IndependentSettingsModule", $"WindowClosed; CloseRequested={_isIndependentSettingsModuleClosing}.");
_isIndependentSettingsModuleClosing = false;
_allowIndependentSettingsModuleRealClose = false;
}
} }
private void InitializeSettingsNavigation() private void InitializeSettingsNavigation()
{ {
_settingsPageDefinitions.Clear();
_settingsNavItems.Clear(); _settingsNavItems.Clear();
_pluginSettingsNavItems.Clear(); InitializePluginSettingsNavigation();
RegisterBuiltInSettingsPageDefinitions();
SettingsPrimaryNavHost.Children.Clear(); RegisterPluginSettingsDefinitions();
SettingsSecondaryNavHost.Children.Clear(); RebuildSettingsNavigationMenu();
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");
} }
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; return;
} }
SelectSettingsTab(tag, persistSelection: true); var selectedTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
SettingsNavView.MenuItems.Clear();
_settingsNavItems.Clear();
_pluginSettingsNavItems.Clear();
IndependentSettingsPageCategory? lastCategory = null;
foreach (var definition in _settingsPageDefinitions.Values
.OrderBy(definition => GetSettingsPageCategoryOrder(definition.Category))
.ThenBy(definition => definition.SortOrder)
.ThenBy(definition => definition.Title, StringComparer.CurrentCulture))
{
if (lastCategory is not null && lastCategory != definition.Category)
{
SettingsNavView.MenuItems.Add(new NavigationViewItemSeparator());
} }
private Button AddSettingsNavItem(Panel host, string tag, Symbol symbol, string title) var navItem = CreateSettingsNavItem(definition);
SettingsNavView.MenuItems.Add(navItem);
_settingsNavItems[definition.Tag] = navItem;
if (definition.Category == IndependentSettingsPageCategory.External)
{ {
var button = CreateSettingsNavItem(tag, symbol, title); _pluginSettingsNavItems[definition.Tag] = navItem;
host.Children.Add(button);
_settingsNavItems[tag] = button;
return button;
} }
private Button CreateSettingsNavItem(string tag, Symbol symbol, string title) lastCategory = definition.Category;
{ }
var icon = new SymbolIcon
{
Symbol = symbol,
IconVariant = IconVariant.Regular
};
icon.Classes.Add("settings-nav-icon");
var iconShell = new Border if (_settingsNavItems.TryGetValue(selectedTag, out var selectedItem))
{ {
Child = icon, SettingsNavView.SelectedItem = selectedItem;
Classes = { "settings-sidebar-icon-shell" } return;
}
if (SettingsNavView.MenuItems.OfType<NavigationViewItem>().FirstOrDefault() is { } firstItem)
{
_selectedSettingsTabTag = firstItem.Tag?.ToString() ?? "General";
SettingsNavView.SelectedItem = firstItem;
}
}
private NavigationViewItem CreateSettingsNavItem(IndependentSettingsPageDefinition definition)
{
var item = new NavigationViewItem
{
Content = definition.Title,
Tag = definition.Tag,
IconSource = new FluentSymbolIconSource
{
Symbol = definition.Icon,
IconVariant = FluentIconVariant.Regular
}
}; };
var label = new TextBlock if (!string.IsNullOrWhiteSpace(definition.ToolTip))
{ {
Text = title, ToolTip.SetTip(item, definition.ToolTip);
Classes = { "settings-nav-label" } }
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
}; };
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() private void InitializeSettingsPageHosts()
{ {
foreach (var button in SettingsPrimaryNavHost.Children.OfType<Button>()) _builtInSettingsPageHosts.Clear();
{
yield return button; 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);
} }
foreach (var button in SettingsSecondaryNavHost.Children.OfType<Button>()) private void RegisterBuiltInSettingsPage(string tag, Control? page)
{ {
yield return button; if (page is not null)
}
foreach (var button in SettingsPluginNavHost.Children.OfType<Button>())
{ {
yield return button; _builtInSettingsPageHosts[tag] = page;
} }
} }
private Button? GetSettingsNavItem(string tag) 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)) if (_settingsNavItems.TryGetValue(tag, out var builtIn))
{ {
@@ -160,51 +343,20 @@ public partial class SettingsWindow
return _pluginSettingsNavItems.GetValueOrDefault(tag); 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) private void SelectSettingsTab(string? tag, bool persistSelection)
{ {
if (string.IsNullOrWhiteSpace(tag)) if (string.IsNullOrWhiteSpace(tag) || SettingsNavView is null)
{ {
return; return;
} }
var selectedButton = GetSettingsNavItem(tag); if (GetSettingsNavItem(tag) is not { } selectedItem)
if (selectedButton is null)
{ {
return; return;
} }
_selectedSettingsTabTag = tag; _selectedSettingsTabTag = tag;
foreach (var button in EnumerateSettingsNavItems()) SettingsNavView.SelectedItem = selectedItem;
{
var isSelected = ReferenceEquals(button, selectedButton);
if (isSelected)
{
if (!button.Classes.Contains("nav-selected"))
{
button.Classes.Add("nav-selected");
}
}
else
{
button.Classes.Remove("nav-selected");
}
}
UpdateSettingsTabContent(); UpdateSettingsTabContent();
if (persistSelection) if (persistSelection)
@@ -220,22 +372,31 @@ public partial class SettingsWindow
private void UpdateSettingsTabContent() private void UpdateSettingsTabContent()
{ {
if (SettingsNavView is null || SettingsPageFrame is null)
{
return;
}
var tag = GetSelectedSettingsTabTag(); 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"; if (tag == "General")
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")
{ {
RenderLauncherHiddenItemsList(); RenderLauncherHiddenItemsList();
} }
@@ -250,11 +411,17 @@ public partial class SettingsWindow
PluginMarketSettingsPanel.RefreshFromRuntime(); PluginMarketSettingsPanel.RefreshFromRuntime();
} }
if (tag == "Grid") if (tag == "Appearance")
{ {
UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout(); UpdateGridPreviewLayout();
} }
if (tag == "Components")
{
UpdateComponentsSettingsSummary();
}
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
SyncVideoWallpaperPreviewPlayback(); SyncVideoWallpaperPreviewPlayback();
} }
@@ -266,6 +433,7 @@ public partial class SettingsWindow
return; return;
} }
AppLogger.Info("IndependentSettingsModule", $"PersistCompleted; Tag='{_selectedSettingsTabTag}'.");
_appSettingsService.Save(BuildAppSettingsSnapshot()); _appSettingsService.Save(BuildAppSettingsSnapshot());
_launcherSettingsService.Save(BuildLauncherSettingsSnapshot()); _launcherSettingsService.Save(BuildLauncherSettingsSnapshot());
} }
@@ -281,7 +449,7 @@ public partial class SettingsWindow
snapshot.WallpaperPath = _wallpaperPath; snapshot.WallpaperPath = _wallpaperPath;
snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement()); snapshot.WallpaperPlacement = GetPlacementDisplayName(GetSelectedWallpaperPlacement());
snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex()); snapshot.SettingsTabIndex = Math.Max(0, GetSettingsTabIndex());
snapshot.SettingsTabTag = GetSelectedSettingsTabTag(); snapshot.SettingsTabTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
snapshot.LanguageCode = _languageCode; snapshot.LanguageCode = _languageCode;
snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id; snapshot.TimeZoneId = _timeZoneService.CurrentTimeZone.Id;
snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode); snapshot.WeatherLocationMode = ToWeatherLocationModeTag(_weatherLocationMode);
@@ -326,6 +494,7 @@ public partial class SettingsWindow
} }
_persistSettingsDebounceTimer?.Dispose(); _persistSettingsDebounceTimer?.Dispose();
AppLogger.Info("IndependentSettingsModule", $"PersistScheduled; DelayMs={Math.Max(0, delayMs)}; Tag='{_selectedSettingsTabTag}'.");
_persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() => _persistSettingsDebounceTimer = DispatcherTimer.RunOnce(() =>
{ {
_persistSettingsDebounceTimer = null; _persistSettingsDebounceTimer = null;
@@ -347,6 +516,22 @@ public partial class SettingsWindow
: "Relaxed"; : "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) private static string NormalizeStatusBarSpacingMode(string? value)
{ {
return value switch return value switch
@@ -590,32 +775,32 @@ public partial class SettingsWindow
private void InitializeSettingsIcons() private void InitializeSettingsIcons()
{ {
const IconVariant variant = IconVariant.Regular; const FluentIconVariant variant = FluentIconVariant.Regular;
WallpaperPlacementSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Wallpaper, IconVariant = variant }; WallpaperPlacementSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Wallpaper, IconVariant = variant };
ThemeColorSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant }; ThemeColorSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
StatusBarClockSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Clock, IconVariant = variant }; StatusBarClockSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Clock, IconVariant = variant };
StatusBarSpacingSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.TextLineSpacing, IconVariant = variant }; StatusBarSpacingSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.TextLineSpacing, IconVariant = variant };
WeatherLocationSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant }; WeatherLocationSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherPreviewSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.WeatherSunny, IconVariant = variant }; WeatherPreviewSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.WeatherSunny, IconVariant = variant };
WeatherAlertFilterSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Info, IconVariant = variant }; WeatherAlertFilterSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Info, IconVariant = variant };
WeatherIconPackSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Color, IconVariant = variant }; WeatherIconPackSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Color, IconVariant = variant };
WeatherNoTlsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Globe, IconVariant = variant }; WeatherNoTlsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Globe, IconVariant = variant };
LanguageSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Translate, IconVariant = variant }; LanguageSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Translate, IconVariant = variant };
TimeZoneSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.GlobeClock, IconVariant = variant }; TimeZoneSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.GlobeClock, IconVariant = variant };
UpdateOptionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowClockwiseDashesSettings, IconVariant = variant }; UpdateOptionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowClockwiseDashesSettings, IconVariant = variant };
UpdateActionsSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.ArrowDownload, IconVariant = variant }; UpdateActionsSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.ArrowDownload, IconVariant = variant };
AboutStartupSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.Play, IconVariant = variant }; AboutStartupSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.Play, IconVariant = variant };
PluginSystemSettingsExpander.IconSource = new SymbolIconSource { Symbol = Symbol.PuzzlePiece, IconVariant = variant }; PluginSystemSettingsExpander.IconSource = new FluentSymbolIconSource { Symbol = FluentSymbol.PuzzlePiece, IconVariant = variant };
UpdateThemeModeIcon(); UpdateThemeModeIcon();
} }
private void UpdateThemeModeIcon() private void UpdateThemeModeIcon()
{ {
ThemeModeSettingsExpander.IconSource = new SymbolIconSource ThemeModeSettingsExpander.IconSource = new FluentSymbolIconSource
{ {
Symbol = _isNightMode ? Symbol.WeatherMoon : Symbol.WeatherSunny, Symbol = _isNightMode ? FluentSymbol.WeatherMoon : FluentSymbol.WeatherSunny,
IconVariant = IconVariant.Regular IconVariant = FluentIconVariant.Regular
}; };
} }

View File

@@ -48,33 +48,50 @@ public partial class SettingsWindow
private void ApplyLocalization() private void ApplyLocalization()
{ {
Title = L("settings.shell.title", "Application Settings"); Title = L("settings.shell.title", "Settings");
WindowTitleTextBlock.Text = L("settings.shell.title", "Application Settings"); WindowTitleTextBlock.Text = L("settings.shell.title", "Settings");
WindowSubtitleTextBlock.Text = L("settings.shell.subtitle", "LanMountainDesktop standalone preferences"); WindowSubtitleTextBlock.Text = L("settings.shell.subtitle", "LanMountainDesktop independent settings module");
WindowVersionBadgeTextBlock.Text = GetAppVersionText(); WindowVersionBadgeTextBlock.Text = GetAppVersionText();
WindowCodeNameBadgeTextBlock.Text = AppCodeName; WindowCodeNameBadgeTextBlock.Text = AppCodeName;
SettingsSidebarTitleTextBlock.Text = L("settings.nav_header", "Settings"); SettingsSidebarTitleTextBlock.Text = L("settings.nav_header", "Settings");
SettingsSidebarHintTextBlock.Text = L( SettingsSidebarHintTextBlock.Text = L(
"settings.shell.sidebar_hint", "settings.shell.sidebar_hint",
"Choose a category to adjust application behavior and desktop appearance."); "Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm.");
SettingsPrimaryGroupTextBlock.Text = L("settings.nav.group_desktop", "Desktop");
SettingsSecondaryGroupTextBlock.Text = L("settings.nav.group_system", "System");
SettingsPluginGroupTextBlock.Text = L("settings.nav.group_extensions", "Extensions");
SettingsSidebarFooterTextBlock.Text = L( SettingsSidebarFooterTextBlock.Text = L(
"settings.shell.footer_hint", "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")); if (GeneralSettingsHubPanel is not null)
SetSettingsNavItemLabel(GetSettingsNavItem("Grid"), L("settings.nav.grid", "Grid")); {
SetSettingsNavItemLabel(GetSettingsNavItem("Color"), L("settings.nav.color", "Color")); GeneralSettingsHubPanel.GeneralPageSubtitleTextBlock.Text = L("settings.page_desc.general", "Manage language, launcher, and weather behavior from the independent settings module.");
SetSettingsNavItemLabel(GetSettingsNavItem("StatusBar"), L("settings.nav.status_bar", "Status Bar")); GeneralSettingsHubPanel.GeneralRegionSectionTitleTextBlock.Text = L("settings.nav.region", "Region");
SetSettingsNavItemLabel(GetSettingsNavItem("Weather"), L("settings.nav.weather", "Weather")); GeneralSettingsHubPanel.GeneralRegionSectionHintTextBlock.Text = L("settings.general.region_hint", "Language and time zone settings affect the entire desktop shell.");
SetSettingsNavItemLabel(GetSettingsNavItem("Region"), L("settings.nav.region", "Region")); GeneralSettingsHubPanel.GeneralLauncherSectionTitleTextBlock.Text = L("settings.nav.launcher", "App Launcher");
SetSettingsNavItemLabel(GetSettingsNavItem("Update"), L("settings.nav.update", "Update")); GeneralSettingsHubPanel.GeneralLauncherSectionHintTextBlock.Text = L("settings.general.launcher_hint", "Restore hidden entries and adjust how the app launcher behaves.");
SetSettingsNavItemLabel(GetSettingsNavItem("About"), L("settings.nav.about", "About")); GeneralSettingsHubPanel.GeneralWeatherSectionTitleTextBlock.Text = L("settings.nav.weather", "Weather");
SetSettingsNavItemLabel(GetSettingsNavItem("Launcher"), L("settings.nav.launcher", "App Launcher")); GeneralSettingsHubPanel.GeneralWeatherSectionHintTextBlock.Text = L("settings.general.weather_hint", "Configure shared weather source, location, and icon style for weather widgets.");
SetSettingsNavItemLabel(GetSettingsNavItem("Plugins"), L("settings.nav.plugins", "Plugins")); }
SetSettingsNavItemLabel(GetSettingsNavItem("PluginMarket"), L("settings.nav.plugin_market", "Plugin Market"));
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"); WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Personalize your wallpaper");
WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement"); WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "Placement");
@@ -211,6 +228,31 @@ public partial class SettingsWindow
ApplyUpdateLocalization(); ApplyUpdateLocalization();
UpdateWallpaperDisplay(); UpdateWallpaperDisplay();
RenderLauncherHiddenItemsList(); 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) private void SetAppRenderModeComboItemContent(string tag, string content)

View File

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

View File

@@ -1,282 +1,312 @@
<Window xmlns="https://github.com/avaloniaui" <local:IndependentSettingsModuleWindowBase xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:fi="using:FluentIcons.Avalonia" xmlns:fi="using:FluentIcons.Avalonia"
xmlns:ic="using:FluentIcons.Avalonia.Fluent" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:pages="using:LanMountainDesktop.Views.SettingsPages"
xmlns:comp="using:LanMountainDesktop.Views.Components" 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:Class="LanMountainDesktop.Views.SettingsWindow"
x:Name="IndependentSettingsModuleWindow"
Title="Settings" Title="Settings"
Icon="/Assets/avalonia-logo.ico" Icon="/Assets/avalonia-logo.ico"
Width="1520" Width="1240"
Height="960" Height="860"
MinWidth="1240" MinWidth="980"
MinHeight="820" MinHeight="680"
ShowInTaskbar="True" ShowInTaskbar="True"
WindowStartupLocation="CenterScreen" 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> <Window.Styles>
<Style Selector="Border.settings-shell-card"> <Style Selector="Border.independent-settings-shell">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassPanelBackgroundBrush}" /> <Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="28" /> <Setter Property="CornerRadius" Value="16" />
<Setter Property="BoxShadow" Value="0 10 28 #12000000" /> <Setter Property="BoxShadow" Value="0 12 36 #22000000" />
</Style> </Style>
<Style Selector="TextBlock.settings-shell-eyebrow"> <Style Selector="TextBlock.independent-settings-hint">
<Setter Property="FontSize" Value="12" /> <Setter Property="Foreground" Value="{DynamicResource TextFillColorSecondaryBrush}" />
<Setter Property="FontWeight" Value="SemiBold" /> <Setter Property="FontSize" Value="12.5" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" />
</Style> </Style>
<Style Selector="TextBlock.settings-shell-hint"> <Style Selector="Border.independent-settings-titlebar">
<Setter Property="FontSize" Value="13" /> <Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextSecondaryBrush}" /> <Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorSecondaryBrush}" />
<Setter Property="BorderThickness" Value="0,0,0,1" />
</Style> </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="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="1" /> <Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="18" /> <Setter Property="Foreground" Value="{DynamicResource TextFillColorPrimaryBrush}" />
<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>
</Style> </Style>
<Style Selector="StackPanel.settings-sidebar-host Button.settings-sidebar-item:pointerover"> <Style Selector="Button.independent-settings-titlebar-button:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveButtonHoverBackgroundBrush}" /> <Setter Property="Background" Value="{DynamicResource SubtleFillColorSecondaryBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveButtonBorderBrush}" />
<Setter Property="RenderTransform" Value="scale(1.01)" />
</Style> </Style>
<Style Selector="StackPanel.settings-sidebar-host Button.settings-sidebar-item.nav-selected"> <Style Selector="Button.independent-settings-titlebar-close:pointerover">
<Setter Property="Background" Value="{DynamicResource AdaptiveNavItemSelectedBackgroundBrush}" /> <Setter Property="Background" Value="#CCB91C1C" />
<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">
<Setter Property="Foreground" Value="White" /> <Setter Property="Foreground" Value="White" />
</Style> </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> </Window.Styles>
<Grid x:Name="DesktopHost"> <Grid x:Name="DesktopHost"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}">
<Border x:Name="DesktopWallpaperLayer" <Border x:Name="DesktopWallpaperLayer"
Background="{DynamicResource AdaptiveSurfaceBaseBrush}" /> IsVisible="False"
Background="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
<Grid x:Name="SettingsPage" <Border Margin="12"
Classes="settings-scope" Classes="independent-settings-shell">
IsVisible="True" <Grid RowDefinitions="48,*">
Opacity="1"
Margin="20">
<Grid x:Name="SettingsContentPanel"
RowDefinitions="Auto,*"
RowSpacing="18">
<Border Grid.Row="0" <Border Grid.Row="0"
Classes="settings-shell-card" Classes="independent-settings-titlebar"
Padding="20,18"> PointerPressed="OnTitleBarPointerPressed"
<Grid ColumnDefinitions="Auto,*,Auto" DoubleTapped="OnTitleBarDoubleTapped">
ColumnSpacing="18"> <Grid ColumnDefinitions="Auto,Auto,*,Auto,Auto"
<Border Width="52" ColumnSpacing="10"
Height="52" Margin="10,0,8,0">
CornerRadius="18" <Button x:Name="SettingsPaneToggleButton"
Background="{DynamicResource AdaptiveAccentBrush}"> Grid.Column="0"
<TextBlock Text="LMD" Classes="independent-settings-pane-button"
FontSize="16" Click="OnSettingsPaneToggleButtonClick"
FontWeight="Bold" ToolTip.Tip="Toggle navigation">
Foreground="White" <fi:SymbolIcon Symbol="Navigation" />
HorizontalAlignment="Center" </Button>
VerticalAlignment="Center" />
</Border>
<StackPanel Grid.Column="1" <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" Orientation="Horizontal"
Spacing="10" Spacing="10"
VerticalAlignment="Center"> VerticalAlignment="Center">
<Border Classes="settings-shell-card" <Border Width="28"
Padding="12,8" Height="28"
CornerRadius="18"> 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" <TextBlock x:Name="WindowVersionBadgeTextBlock"
FontSize="14" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
FontWeight="SemiBold" FontWeight="SemiBold"
Text="1.0.0" /> Text="1.0.0" />
</Border> </Border>
<Border Classes="settings-shell-card" <Border Classes="settings-page-shell"
Padding="12,8" Padding="10,4"
CornerRadius="18"> CornerRadius="12">
<TextBlock x:Name="WindowCodeNameBadgeTextBlock" <TextBlock x:Name="WindowCodeNameBadgeTextBlock"
Classes="settings-shell-hint" Classes="independent-settings-hint"
Text="Administrate" /> Text="Administrate" />
</Border> </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> </StackPanel>
</Grid> </Grid>
</Border> </Border>
<Grid Grid.Row="1" <Grid Grid.Row="1">
ColumnDefinitions="300,20,*"> <ui:NavigationView x:Name="SettingsNavView"
<Border Grid.Column="0" SelectionChanged="OnSettingsNavSelectionChanged">
Classes="settings-shell-card" <ui:NavigationView.PaneHeader>
Padding="18,18,18,16"> <StackPanel Margin="10,10,10,12"
<Grid RowDefinitions="Auto,*,Auto" Spacing="4">
RowSpacing="18">
<StackPanel Spacing="6">
<TextBlock x:Name="SettingsSidebarTitleTextBlock" <TextBlock x:Name="SettingsSidebarTitleTextBlock"
Classes="settings-shell-eyebrow" FontSize="15"
FontWeight="SemiBold"
Text="Settings" /> Text="Settings" />
<TextBlock x:Name="SettingsSidebarHintTextBlock" <TextBlock x:Name="SettingsSidebarHintTextBlock"
Classes="settings-shell-hint" Classes="independent-settings-hint"
TextWrapping="Wrap" 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> </StackPanel>
</ui:NavigationView.PaneHeader>
<ScrollViewer Grid.Row="1" <ui:NavigationView.PaneFooter>
HorizontalScrollBarVisibility="Disabled" <Border Margin="10,10,10,12"
VerticalScrollBarVisibility="Auto"> Classes="settings-page-shell"
<StackPanel Spacing="20"> Padding="12">
<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" <TextBlock x:Name="SettingsSidebarFooterTextBlock"
Classes="settings-shell-hint" Classes="independent-settings-hint"
TextWrapping="Wrap" TextWrapping="Wrap"
Text="Tray-opened settings are managed in this standalone window." /> Text="Both the tray entry and the in-app settings entry now open this same independent settings module." />
</StackPanel>
</Border>
</Grid>
</Border> </Border>
</ui:NavigationView.PaneFooter>
<Grid Grid.Column="2" <Grid RowDefinitions="Auto,*">
RowDefinitions="*,Auto" <ui:InfoBar x:Name="IndependentSettingsStatusInfoBar"
RowSpacing="14"> Title="Independent settings module status"
<Border Grid.Row="0" IsClosable="True"
Classes="settings-shell-card" IsOpen="False"
Padding="0"> Severity="Warning" />
<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="General" />
<TextBlock x:Name="CurrentSettingsPageSubtitleTextBlock"
Classes="independent-settings-hint"
MaxWidth="720"
TextWrapping="Wrap"
Text="Configure this part of LanMountainDesktop in the independent settings module." />
</StackPanel>
<Border Grid.Row="1"
Classes="settings-page-shell"
MaxWidth="980"
HorizontalAlignment="Left">
<ScrollViewer x:Name="SettingsContentScrollViewer" <ScrollViewer x:Name="SettingsContentScrollViewer"
Padding="30,28,30,30"
HorizontalScrollBarVisibility="Disabled" HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"> VerticalScrollBarVisibility="Auto">
<Grid x:Name="SettingsContentPagesHost"> <ui:Frame x:Name="SettingsPageFrame"
<pages:WallpaperSettingsPage x:Name="WallpaperSettingsPanel" IsVisible="True" /> Margin="0" />
<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> </ScrollViewer>
</Border> </Border>
<Border x:Name="PendingRestartDock" <Border x:Name="PendingRestartDock"
Grid.Row="1" Grid.Row="2"
IsVisible="False" IsVisible="False"
Classes="settings-shell-card" Margin="0,14,0,0"
Padding="16,14" Classes="settings-page-shell"
CornerRadius="24"> Padding="14,12"
MaxWidth="980"
HorizontalAlignment="Left">
<Grid ColumnDefinitions="Auto,*,Auto" <Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="14"> ColumnSpacing="12">
<Border Width="38" <Border Width="36"
Height="38" Height="36"
CornerRadius="14" CornerRadius="18"
Background="{DynamicResource AdaptiveAccentBrush}"> Background="{DynamicResource AccentFillColorDefaultBrush}">
<fi:FluentIcon Icon="ArrowSync" <fi:FluentIcon Icon="ArrowSync"
IconVariant="Regular" IconVariant="Regular"
FontSize="18" FontSize="16"
Foreground="White" Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</Border> </Border>
<StackPanel Grid.Column="1" <StackPanel Grid.Column="1"
Spacing="2" Spacing="2"
VerticalAlignment="Center"> VerticalAlignment="Center">
@@ -285,13 +315,14 @@
FontWeight="SemiBold" FontWeight="SemiBold"
Text="Restart required" /> Text="Restart required" />
<TextBlock x:Name="PendingRestartDockDescriptionTextBlock" <TextBlock x:Name="PendingRestartDockDescriptionTextBlock"
Classes="independent-settings-hint"
TextWrapping="Wrap" TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}"
Text="Your changes will apply after restarting the app." /> Text="Your changes will apply after restarting the app." />
</StackPanel> </StackPanel>
<Button x:Name="PendingRestartDockButton" <Button x:Name="PendingRestartDockButton"
Grid.Column="2" Grid.Column="2"
Padding="16,8" Padding="14,8"
Click="OnPendingRestartDockButtonClick"> Click="OnPendingRestartDockButtonClick">
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<fi:FluentIcon Icon="ArrowSync" <fi:FluentIcon Icon="ArrowSync"
@@ -305,8 +336,10 @@
</Border> </Border>
</Grid> </Grid>
</Grid> </Grid>
</ui:NavigationView>
</Grid> </Grid>
</Grid> </Grid>
</Border>
<Grid IsVisible="False"> <Grid IsVisible="False">
<Button x:Name="BackToWindowsButton" /> <Button x:Name="BackToWindowsButton" />
@@ -324,4 +357,4 @@
<Border x:Name="BottomTaskbarContainer" /> <Border x:Name="BottomTaskbarContainer" />
</Grid> </Grid>
</Grid> </Grid>
</Window> </local:IndependentSettingsModuleWindowBase>

View File

@@ -1,11 +1,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@@ -22,12 +25,13 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components; using LanMountainDesktop.Views.Components;
using LanMountainDesktop.Views.SettingsPages;
using LibVLCSharp.Shared; using LibVLCSharp.Shared;
using Line = Avalonia.Controls.Shapes.Line; using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views; namespace LanMountainDesktop.Views;
public partial class SettingsWindow : Window public partial class SettingsWindow : IndependentSettingsModuleWindowBase
{ {
private enum WallpaperPlacement private enum WallpaperPlacement
{ {
@@ -102,8 +106,23 @@ public partial class SettingsWindow : Window
private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = []; private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
private readonly Dictionary<string, Button> _settingsNavItems = new(StringComparer.OrdinalIgnoreCase); private readonly Dictionary<string, NavigationViewItem> _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Button> _pluginSettingsNavItems = 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 StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes; private byte[]? _launcherFolderIconPngBytes;
@@ -168,20 +187,28 @@ public partial class SettingsWindow : Window
private bool _weatherNoTlsRequests; private bool _weatherNoTlsRequests;
private bool _autoStartWithWindows; private bool _autoStartWithWindows;
private string _weatherSearchKeyword = string.Empty; private string _weatherSearchKeyword = string.Empty;
private string _selectedSettingsTabTag = "Wallpaper"; private string _selectedSettingsTabTag = "General";
private WallpaperPlacement _selectedWallpaperPlacement = WallpaperPlacement.Fill;
private bool _isWeatherSearchInProgress; private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress; private bool _isWeatherPreviewInProgress;
private bool _controlsBound;
private bool _independentModuleInitializationCompleted;
private bool _suppressWallpaperPlacementEvents;
private bool _isIndependentSettingsModuleClosing;
private bool _allowIndependentSettingsModuleRealClose;
public SettingsWindow() public SettingsWindow()
{ {
_componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService); _componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService);
InitializeComponent(); InitializeComponent();
InitializeSettingsPageHosts();
InitializeSettingsNavigation(); InitializeSettingsNavigation();
InitializePluginSettingsNavigation(); InitializePluginSettingsNavigation();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault(); _fluentAvaloniaTheme = Application.Current?.Styles.OfType<FluentAvaloniaTheme>().FirstOrDefault();
RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default; RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged; PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
HookEvents(); Closing += OnIndependentSettingsModuleClosing;
Opened += OnWindowOpened;
} }
private void InitializeComponent() private void InitializeComponent()
@@ -235,7 +262,27 @@ public partial class SettingsWindow : Window
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick; DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled; AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled;
AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged; 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) private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
@@ -273,10 +320,70 @@ public partial class SettingsWindow : Window
private void OnWindowOpened(object? sender, EventArgs e) private void OnWindowOpened(object? sender, EventArgs e)
{ {
Opened -= OnWindowOpened; Opened -= OnWindowOpened;
_suppressSettingsPersistence = true; UpdateWindowChromeState();
var snapshot = _appSettingsService.Load(); UiExceptionGuard.FireAndForgetGuarded(
var launcherSnapshot = _launcherSettingsService.Load(); 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;
});
}
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)
{
e.Cancel = true;
PersistSettings();
Hide();
AppLogger.Info("IndependentSettingsModule", "WindowHiddenByClose.");
return;
}
_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( _targetShortSideCells = Math.Clamp(
snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(), snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
MinShortSideCells, MinShortSideCells,
@@ -310,7 +417,15 @@ public partial class SettingsWindow : Window
InitializeLauncherVisibilitySettings(launcherSnapshot); InitializeLauncherVisibilitySettings(launcherSnapshot);
InitializeSettingsIcons(); InitializeSettingsIcons();
ApplyLocalization(); ApplyLocalization();
return Task.CompletedTask;
});
await RunInitializationStageAsync("VisualState", () =>
{
_selectedWallpaperPlacement = GetWallpaperPlacementFromIndex(GetPlacementIndexFromSetting(snapshot.WallpaperPlacement));
_suppressWallpaperPlacementEvents = true;
WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement); WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
_suppressWallpaperPlacementEvents = false;
TryRestoreWallpaper(snapshot.WallpaperPath); TryRestoreWallpaper(snapshot.WallpaperPath);
RefreshColorPalettes(); RefreshColorPalettes();
if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
@@ -330,12 +445,162 @@ public partial class SettingsWindow : Window
UpdateWallpaperPreviewLayout(); UpdateWallpaperPreviewLayout();
UpdateGridPreviewLayout(); UpdateGridPreviewLayout();
InitializeTimeZoneSettings(); InitializeTimeZoneSettings();
_ = LoadLauncherEntriesAsync(); 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; _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) private void OnCloseWindowClick(object? sender, RoutedEventArgs e)
{ {
Close(); 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); Directory.CreateDirectory(dataDirectory);
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory); ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
AppLogger.Info(
"PluginLoader",
$"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames); loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames);
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath); var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
AppLogger.Info("PluginLoader", $"Assembly loaded. PluginId='{manifest.Id}'; Assembly='{assembly.FullName}'.");
var pluginType = ResolvePluginType(assembly); var pluginType = ResolvePluginType(assembly);
plugin = CreatePluginInstance(pluginType); plugin = CreatePluginInstance(pluginType);
AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties); runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
var serviceCollection = CreateServiceCollection(runtimeContext, services); var serviceCollection = CreateServiceCollection(runtimeContext, services);
var hostBuilderContext = CreateHostBuilderContext(runtimeContext); var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
plugin.Initialize(hostBuilderContext, serviceCollection); plugin.Initialize(hostBuilderContext, serviceCollection);
AppLogger.Info("PluginLoader", $"Plugin Initialize completed. PluginId='{manifest.Id}'.");
pluginServices = serviceCollection.BuildServiceProvider(new ServiceProviderOptions pluginServices = serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{ {
ValidateScopes = false, ValidateScopes = false,
ValidateOnBuild = true ValidateOnBuild = true
}); });
AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices); runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices var settingsPages = pluginServices
@@ -174,8 +181,12 @@ public sealed class PluginLoader
.ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase) .ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray(); .ToArray();
var exportedServices = ResolveExports(manifest, pluginServices); 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(); hostedServices = pluginServices.GetServices<IHostedService>().ToArray();
StartHostedServices(hostedServices); StartHostedServices(hostedServices);
AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
var loadedPlugin = new LoadedPlugin( var loadedPlugin = new LoadedPlugin(
manifest, manifest,
@@ -375,6 +386,7 @@ public sealed class PluginLoader
{ {
foreach (var hostedService in hostedServices) foreach (var hostedService in hostedServices)
{ {
AppLogger.Info("PluginLoader", $"Starting hosted service '{hostedService.GetType().FullName}'.");
hostedService.StartAsync(CancellationToken.None).GetAwaiter().GetResult(); hostedService.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
} }
} }

View File

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

View File

@@ -31,14 +31,8 @@ internal static class AirAppMarketDefaults
public static string? TryGetWorkspaceIndexPath() public static string? TryGetWorkspaceIndexPath()
{ {
var repositoryRoot = TryGetWorkspaceRepositoryRoot("LanAirApp"); var relativePath = Path.Combine("airappmarket", "index.json");
if (repositoryRoot is null) return TryResolveWorkspacePath("LanAirApp", relativePath);
{
return null;
}
var candidatePath = Path.Combine(repositoryRoot, "airappmarket", "index.json");
return File.Exists(candidatePath) ? candidatePath : null;
} }
public static bool TryResolveWorkspaceFile(string url, out string localPath) public static bool TryResolveWorkspaceFile(string url, out string localPath)
@@ -57,14 +51,8 @@ internal static class AirAppMarketDefaults
return false; return false;
} }
var repositoryRoot = TryGetWorkspaceRepositoryRoot(repositoryName); var candidatePath = TryResolveWorkspacePath(repositoryName, relativePath);
if (repositoryRoot is null) if (candidatePath is null)
{
return false;
}
var candidatePath = Path.GetFullPath(Path.Combine(repositoryRoot, relativePath));
if (!File.Exists(candidatePath))
{ {
return false; return false;
} }
@@ -99,7 +87,7 @@ internal static class AirAppMarketDefaults
return !string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repositoryName); 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); var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null) while (current is not null)
@@ -107,7 +95,11 @@ internal static class AirAppMarketDefaults
var candidate = Path.Combine(current.FullName, repositoryName); var candidate = Path.Combine(current.FullName, repositoryName);
if (Directory.Exists(candidate)) if (Directory.Exists(candidate))
{ {
return candidate; var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
if (File.Exists(candidatePath))
{
return candidatePath;
}
} }
current = current.Parent; 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:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:ui="using:FluentAvalonia.UI.Controls"
mc:Ignorable="d" mc:Ignorable="d"
d:DesignWidth="960" d:DesignWidth="960"
d:DesignHeight="1000" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginMarketSettingsPage"> x:Class="LanMountainDesktop.Views.SettingsPages.PluginMarketSettingsPage">
<StackPanel x:Name="PluginMarketPanel" <StackPanel x:Name="PluginMarketPanel"
MaxWidth="920"
Spacing="16"> Spacing="16">
<TextBlock x:Name="PluginMarketPanelTitleTextBlock" <TextBlock x:Name="PluginMarketPanelTitleTextBlock"
IsVisible="False"
FontSize="24" FontSize="24"
FontWeight="SemiBold" FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Text="Plugin Market" /> Text="Plugin Market" />
<TextBlock x:Name="PluginMarketPanelSubtitleTextBlock" <TextBlock x:Name="PluginMarketPanelSubtitleTextBlock"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" Foreground="{DynamicResource TextFillColorSecondaryBrush}"
TextWrapping="Wrap" 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." />
<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" /> <ContentControl x:Name="PluginMarketContentHost" />
</StackPanel> </StackPanel>
</Border>
</StackPanel>
</UserControl> </UserControl>

View File

@@ -66,6 +66,7 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory); Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions(); ApplyPendingPluginDeletions();
UnloadInstalledPlugins(); UnloadInstalledPlugins();
AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds(); var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load(); var settingsSnapshot = _appSettingsService.Load();
@@ -81,6 +82,9 @@ public sealed class PluginRuntimeService : IDisposable
var discoveryFailures = new List<PluginLoadResult>(); var discoveryFailures = new List<PluginLoadResult>();
var candidates = DiscoverCandidates(discoveryFailures); var candidates = DiscoverCandidates(discoveryFailures);
_loadResults.AddRange(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); var selectedPluginIds = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates) foreach (var candidate in candidates)
@@ -93,6 +97,7 @@ public sealed class PluginRuntimeService : IDisposable
new InvalidOperationException( new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected.")); $"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
_loadResults.Add(duplicateFailure); _loadResults.Add(duplicateFailure);
LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
continue; continue;
} }
@@ -113,7 +118,13 @@ public sealed class PluginRuntimeService : IDisposable
try try
{ {
AppLogger.Info(
"PluginRuntime",
$"Preparing shared contracts. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
RegisterSharedContractsForLoad(candidate.Manifest); RegisterSharedContractsForLoad(candidate.Manifest);
AppLogger.Info(
"PluginRuntime",
$"Shared contracts ready. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'.");
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -128,10 +139,13 @@ public sealed class PluginRuntimeService : IDisposable
ex.Message, ex.Message,
0, 0,
0)); 0));
Debug.WriteLine($"[PluginRuntime] Failed to prepare dependencies for '{candidate.Manifest.Id}': {ex}"); LogPluginFailure("DependencyPrepare", dependencyFailure, treatAsError: false);
continue; continue;
} }
AppLogger.Info(
"PluginRuntime",
$"Starting plugin load. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
var loadResult = candidate.SourceKind switch var loadResult = candidate.SourceKind switch
{ {
PluginCatalogSourceKind.Package => _loader.LoadFromPackage( PluginCatalogSourceKind.Package => _loader.LoadFromPackage(
@@ -160,6 +174,9 @@ public sealed class PluginRuntimeService : IDisposable
null, null,
loadResult.LoadedPlugin.SettingsPages.Count, loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.DesktopComponents.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}'."); Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue; continue;
} }
@@ -173,11 +190,15 @@ public sealed class PluginRuntimeService : IDisposable
loadResult.Error?.Message, loadResult.Error?.Message,
0, 0,
0)); 0));
LogPluginFailure("Load", loadResult, treatAsError: true);
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}"); Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
} }
if (_catalog.Count == 0 && discoveryFailures.Count == 0) 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}'."); 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) 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) 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); 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) private void RemovePluginFromSnapshot(string pluginId)
{ {
var snapshot = _appSettingsService.Load(); var snapshot = _appSettingsService.Load();

View File

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

View File

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

View File

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