diff --git a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj
index 3856220..f7cc095 100644
--- a/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj
+++ b/LanAirApp/samples/LanMountainDesktop.SamplePlugin/LanMountainDesktop.SamplePlugin.csproj
@@ -9,9 +9,9 @@
bin\$(Configuration)\$(TargetFramework)\content\
false
false
- ..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\
- $(PluginPackageOutputDirectory)$(AssemblyName).laapp
- ..\..\..\LanMountainDesktop\bin\$(Configuration)\$(TargetFramework)\Extensions\Plugins\SamplePlugin\
+ $(MSBuildThisFileDirectory)artifacts\Packages\
+ $(PluginPackageOutputDirectory)$(AssemblyName).$(Version).laapp
+ $(MSBuildThisFileDirectory)artifacts\Loose\
diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index 22e5397..e6d9094 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -1,32 +1,50 @@
+using System;
+using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
-using System;
-using System.Linq;
using Avalonia.Markup.Xaml;
using Avalonia.Platform;
using Avalonia.Threading;
+using AvaloniaWebView;
using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services;
using LanMountainDesktop.ViewModels;
using LanMountainDesktop.Views;
-using AvaloniaWebView;
-using LanMountainDesktop.PluginSdk;
namespace LanMountainDesktop;
public partial class App : Application
{
+ private enum DesktopShellState
+ {
+ ForegroundDesktop = 0,
+ MinimizedToTaskbar = 1,
+ TrayOnly = 2
+ }
+
+ private enum ShutdownIntent
+ {
+ None = 0,
+ ExitRequested = 1,
+ RestartRequested = 2
+ }
+
private readonly AppSettingsService _appSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService();
private bool _exitCleanupCompleted;
+ private DesktopShellState _desktopShellState = DesktopShellState.ForegroundDesktop;
+ private ShutdownIntent _shutdownIntent;
- private SettingsWindow? _traySettingsWindow;
+ private readonly IndependentSettingsModuleService _independentSettingsModuleService = new();
private TrayIcons? _trayIcons;
private PluginRuntimeService? _pluginRuntimeService;
+ private MainWindow? _mainWindow;
+ private bool _mainWindowClosed;
internal static SingleInstanceService? CurrentSingleInstanceService { get; set; }
internal static IHostApplicationLifecycle? CurrentHostApplicationLifecycle =>
@@ -35,6 +53,11 @@ public partial class App : Application
public PluginRuntimeService? PluginRuntimeService => _pluginRuntimeService;
public IHostApplicationLifecycle HostApplicationLifecycle => _hostApplicationLifecycle;
+ internal void OpenIndependentSettingsModule(string source, string? pageTag = null)
+ {
+ _independentSettingsModuleService.ShowOrActivate(source, pageTag);
+ }
+
public override void Initialize()
{
AppLogger.Info("App", "Initializing application resources.");
@@ -62,12 +85,8 @@ public partial class App : Application
AppLogger.Info("App", "Desktop lifetime exit triggered.");
PerformExitCleanup();
};
- desktop.MainWindow = new MainWindow
- {
- DataContext = new MainWindowViewModel(),
- };
- AppLogger.Info("App", $"Main window created. LogFile={AppLogger.LogFilePath}");
- LogBrowserStartupDiagnostics();
+
+ CreateAndAssignMainWindow(desktop, "FrameworkInitialization");
CurrentSingleInstanceService?.StartActivationListener(ActivateMainWindow);
}
@@ -81,42 +100,14 @@ public partial class App : Application
Reason: "User selected Exit App from the tray menu."));
}
+ private void OnTrayShowDesktopClick(object? sender, EventArgs e)
+ {
+ RestoreOrCreateMainWindow(showSingleInstanceNotice: false, source: "TrayMenu");
+ }
+
private void OnTraySettingsClick(object? sender, EventArgs e)
{
- if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime)
- {
- return;
- }
-
- Dispatcher.UIThread.Post(() =>
- {
- try
- {
- if (_traySettingsWindow is { } existingWindow && existingWindow.IsVisible)
- {
- existingWindow.WindowState = Avalonia.Controls.WindowState.Normal;
- existingWindow.Activate();
- return;
- }
-
- var settingsWindow = new SettingsWindow();
- settingsWindow.Closed += (_, _) =>
- {
- if (ReferenceEquals(_traySettingsWindow, settingsWindow))
- {
- _traySettingsWindow = null;
- }
- };
-
- _traySettingsWindow = settingsWindow;
- settingsWindow.Show();
- settingsWindow.Activate();
- }
- catch (Exception ex)
- {
- AppLogger.Warn("TraySettings", "Failed to open settings window.", ex);
- }
- }, DispatcherPriority.Normal);
+ OpenIndependentSettingsModule("TrayMenu");
}
private void OnTrayRestartClick(object? sender, EventArgs e)
@@ -209,19 +200,25 @@ public partial class App : Application
{
var menu = new NativeMenu();
- var settingsItem = new NativeMenuItem(L("tray.menu.settings", "设置"));
+ var showDesktopItem = new NativeMenuItem(L("tray.menu.show_desktop", "Open Desktop"));
+ showDesktopItem.Click += OnTrayShowDesktopClick;
+ menu.Items.Add(showDesktopItem);
+
+ menu.Items.Add(new NativeMenuItemSeparator());
+
+ var settingsItem = new NativeMenuItem(L("tray.menu.settings", "Settings"));
settingsItem.Click += OnTraySettingsClick;
menu.Items.Add(settingsItem);
menu.Items.Add(new NativeMenuItemSeparator());
- var restartItem = new NativeMenuItem(L("tray.menu.restart", "重启应用"));
+ var restartItem = new NativeMenuItem(L("tray.menu.restart", "Restart App"));
restartItem.Click += OnTrayRestartClick;
menu.Items.Add(restartItem);
menu.Items.Add(new NativeMenuItemSeparator());
- var exitItem = new NativeMenuItem(L("tray.menu.exit", "退出应用"));
+ var exitItem = new NativeMenuItem(L("tray.menu.exit", "Exit App"));
exitItem.Click += OnTrayExitClick;
menu.Items.Add(exitItem);
@@ -245,6 +242,11 @@ public partial class App : Application
}
private void ActivateMainWindow()
+ {
+ RestoreOrCreateMainWindow(showSingleInstanceNotice: true, source: "SingleInstance");
+ }
+
+ private void RestoreOrCreateMainWindow(bool showSingleInstanceNotice, string source)
{
Dispatcher.UIThread.Post(() =>
{
@@ -253,13 +255,11 @@ public partial class App : Application
return;
}
- if (desktop.MainWindow is not Window mainWindow)
- {
- return;
- }
-
try
{
+ var mainWindow = GetOrCreateMainWindow(desktop, source);
+ mainWindow.ShowInTaskbar = true;
+
if (!mainWindow.IsVisible)
{
mainWindow.Show();
@@ -278,18 +278,68 @@ public partial class App : Application
mainWindow.Activate();
mainWindow.Topmost = true;
mainWindow.Topmost = false;
- if (mainWindow is MainWindow lanMountainMainWindow)
+ SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"Restore:{source}");
+ AppLogger.Info(
+ "DesktopShell",
+ $"Desktop restored. Source='{source}'; MainWindowClosed={_mainWindowClosed}; ShowSingleInstanceNotice={showSingleInstanceNotice}; WindowState='{mainWindow.WindowState}'.");
+
+ if (showSingleInstanceNotice)
{
- lanMountainMainWindow.ShowSingleInstanceNotice();
+ mainWindow.ShowSingleInstanceNotice();
}
}
catch (Exception ex)
{
- AppLogger.Warn("SingleInstance", "Failed to activate the existing main window.", ex);
+ AppLogger.Warn("DesktopShell", $"Failed to restore desktop shell. Source='{source}'.", ex);
}
}, DispatcherPriority.Send);
}
+ internal void PrepareForShutdown(bool isRestart, string source)
+ {
+ void Mark()
+ {
+ _shutdownIntent = isRestart
+ ? ShutdownIntent.RestartRequested
+ : ShutdownIntent.ExitRequested;
+ AppLogger.Info(
+ "DesktopShell",
+ $"Shutdown intent marked. Intent='{_shutdownIntent}'; Source='{source}'; CurrentShellState='{_desktopShellState}'.");
+ }
+
+ if (Dispatcher.UIThread.CheckAccess())
+ {
+ Mark();
+ return;
+ }
+
+ Dispatcher.UIThread.InvokeAsync(Mark, DispatcherPriority.Send).GetAwaiter().GetResult();
+ }
+
+ internal void ResetShutdownIntent(string source)
+ {
+ void Reset()
+ {
+ if (_shutdownIntent == ShutdownIntent.None)
+ {
+ return;
+ }
+
+ AppLogger.Warn(
+ "DesktopShell",
+ $"Shutdown intent cleared without process exit. PreviousIntent='{_shutdownIntent}'; Source='{source}'.");
+ _shutdownIntent = ShutdownIntent.None;
+ }
+
+ if (Dispatcher.UIThread.CheckAccess())
+ {
+ Reset();
+ return;
+ }
+
+ Dispatcher.UIThread.InvokeAsync(Reset, DispatcherPriority.Send).GetAwaiter().GetResult();
+ }
+
private void OnAppSettingsSaved(string _)
{
Dispatcher.UIThread.Post(() =>
@@ -311,18 +361,7 @@ public partial class App : Application
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
- try
- {
- _traySettingsWindow?.Close();
- }
- catch (Exception ex)
- {
- AppLogger.Warn("App", "Failed to close tray-opened settings window during shutdown.", ex);
- }
- finally
- {
- _traySettingsWindow = null;
- }
+ _independentSettingsModuleService.CloseIfOpen();
try
{
@@ -342,6 +381,171 @@ public partial class App : Application
DisposeTrayIcon();
}
+ private MainWindow CreateAndAssignMainWindow(
+ IClassicDesktopStyleApplicationLifetime desktop,
+ string reason)
+ {
+ var mainWindow = new MainWindow
+ {
+ DataContext = new MainWindowViewModel(),
+ ShowInTaskbar = true
+ };
+
+ AttachMainWindow(mainWindow);
+ desktop.MainWindow = mainWindow;
+ AppLogger.Info("App", $"Main window created. Reason='{reason}'. LogFile={AppLogger.LogFilePath}");
+ LogBrowserStartupDiagnostics();
+ SetDesktopShellState(DesktopShellState.ForegroundDesktop, $"MainWindowCreated:{reason}");
+ return mainWindow;
+ }
+
+ private MainWindow GetOrCreateMainWindow(
+ IClassicDesktopStyleApplicationLifetime desktop,
+ string reason)
+ {
+ if (_mainWindow is not null && !_mainWindowClosed)
+ {
+ return _mainWindow;
+ }
+
+ if (desktop.MainWindow is MainWindow desktopMainWindow && !_mainWindowClosed)
+ {
+ AttachMainWindow(desktopMainWindow);
+ return desktopMainWindow;
+ }
+
+ return CreateAndAssignMainWindow(desktop, reason);
+ }
+
+ private void AttachMainWindow(MainWindow mainWindow)
+ {
+ if (ReferenceEquals(_mainWindow, mainWindow))
+ {
+ _mainWindowClosed = false;
+ return;
+ }
+
+ if (_mainWindow is not null)
+ {
+ _mainWindow.Closing -= OnMainWindowClosing;
+ _mainWindow.Closed -= OnMainWindowClosed;
+ _mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
+ }
+
+ _mainWindow = mainWindow;
+ _mainWindowClosed = false;
+ mainWindow.Closing += OnMainWindowClosing;
+ mainWindow.Closed += OnMainWindowClosed;
+ mainWindow.PropertyChanged += OnMainWindowPropertyChanged;
+ }
+
+ private void OnMainWindowClosing(object? sender, WindowClosingEventArgs e)
+ {
+ if (sender is not MainWindow mainWindow)
+ {
+ return;
+ }
+
+ AppLogger.Info(
+ "DesktopShell",
+ $"Main window closing requested. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'; WindowState='{mainWindow.WindowState}'; IsVisible={mainWindow.IsVisible}.");
+
+ if (_shutdownIntent is ShutdownIntent.ExitRequested or ShutdownIntent.RestartRequested)
+ {
+ AppLogger.Info(
+ "DesktopShell",
+ $"Main window close allowed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
+ return;
+ }
+
+ e.Cancel = true;
+ HideMainWindowToTray(mainWindow, "MainWindowClosing");
+ }
+
+ private void OnMainWindowClosed(object? sender, EventArgs e)
+ {
+ if (sender is not MainWindow mainWindow)
+ {
+ return;
+ }
+
+ mainWindow.Closing -= OnMainWindowClosing;
+ mainWindow.Closed -= OnMainWindowClosed;
+ mainWindow.PropertyChanged -= OnMainWindowPropertyChanged;
+
+ if (ReferenceEquals(_mainWindow, mainWindow))
+ {
+ _mainWindow = null;
+ }
+
+ _mainWindowClosed = true;
+ AppLogger.Info(
+ "DesktopShell",
+ $"Main window closed. Intent='{_shutdownIntent}'; ShellState='{_desktopShellState}'.");
+
+ if (_shutdownIntent == ShutdownIntent.None)
+ {
+ SetDesktopShellState(DesktopShellState.TrayOnly, "MainWindowClosedUnexpected");
+ }
+ }
+
+ private void OnMainWindowPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (sender is not MainWindow mainWindow)
+ {
+ return;
+ }
+
+ if (e.Property != Window.WindowStateProperty)
+ {
+ return;
+ }
+
+ if (_shutdownIntent != ShutdownIntent.None || !mainWindow.IsVisible)
+ {
+ return;
+ }
+
+ if (mainWindow.WindowState == WindowState.Minimized)
+ {
+ SetDesktopShellState(DesktopShellState.MinimizedToTaskbar, "MainWindowMinimized");
+ return;
+ }
+
+ SetDesktopShellState(DesktopShellState.ForegroundDesktop, "MainWindowRestored");
+ }
+
+ private void HideMainWindowToTray(MainWindow mainWindow, string source)
+ {
+ try
+ {
+ mainWindow.ShowInTaskbar = false;
+ mainWindow.Hide();
+ SetDesktopShellState(DesktopShellState.TrayOnly, source);
+ AppLogger.Info(
+ "DesktopShell",
+ $"Main window hidden to tray. Source='{source}'; WindowState='{mainWindow.WindowState}'.");
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("DesktopShell", $"Failed to hide main window to tray. Source='{source}'.", ex);
+ }
+ }
+
+ private void SetDesktopShellState(DesktopShellState state, string source)
+ {
+ if (_desktopShellState == state)
+ {
+ return;
+ }
+
+ var previous = _desktopShellState;
+ _desktopShellState = state;
+ AppLogger.Info(
+ "DesktopShell",
+ $"Shell state changed. Previous='{previous}'; Current='{state}'; Source='{source}'.");
+ }
+
private void LogBrowserStartupDiagnostics()
{
try
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index 7a60ca1..dfee34e 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -1,6 +1,7 @@
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
+ "tray.menu.show_desktop": "Open Desktop",
"tray.menu.settings": "Settings",
"tray.menu.restart": "Restart App",
"tray.menu.exit": "Exit App",
@@ -8,10 +9,10 @@
"tooltip.back_to_windows": "Back to Windows",
"tooltip.open_settings": "Settings",
"settings.title": "Settings",
- "settings.shell.title": "Application Settings",
- "settings.shell.subtitle": "LanMountainDesktop standalone preferences",
+ "settings.shell.title": "Settings",
+ "settings.shell.subtitle": "LanMountainDesktop independent settings module",
"settings.shell.sidebar_hint": "Choose a category to adjust application behavior, desktop layout, and appearance.",
- "settings.shell.footer_hint": "Tray-opened settings are managed in this standalone window.",
+ "settings.shell.footer_hint": "Tray-opened settings are managed in this independent settings module.",
"settings.back_to_desktop": "Back to Desktop",
"settings.nav_header": "Settings",
"settings.nav.group_desktop": "Desktop",
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index 9ba6b7e..f33fbc7 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -1,6 +1,7 @@
{
"app.title": "LanMountainDesktop",
"tray.tooltip": "LanMountainDesktop",
+ "tray.menu.show_desktop": "打开桌面",
"tray.menu.settings": "设置",
"tray.menu.restart": "重启应用",
"tray.menu.exit": "退出应用",
@@ -8,10 +9,10 @@
"tooltip.back_to_windows": "回到Windows",
"tooltip.open_settings": "设置",
"settings.title": "设置",
- "settings.shell.title": "应用设置",
- "settings.shell.subtitle": "LanMountainDesktop 独立设置窗口",
+ "settings.shell.title": "设置",
+ "settings.shell.subtitle": "LanMountainDesktop 独立设置模块",
"settings.shell.sidebar_hint": "选择一个分类以调整应用行为、桌面布局与外观。",
- "settings.shell.footer_hint": "托盘菜单打开的设置会统一在这个独立窗口中管理。",
+ "settings.shell.footer_hint": "托盘菜单打开的设置会统一在这个独立设置模块中管理。",
"settings.back_to_desktop": "返回桌面",
"settings.nav_header": "设置选项",
"settings.nav.group_desktop": "桌面",
diff --git a/LanMountainDesktop/Services/HostApplicationLifecycleService.cs b/LanMountainDesktop/Services/HostApplicationLifecycleService.cs
index ec6717e..5af6817 100644
--- a/LanMountainDesktop/Services/HostApplicationLifecycleService.cs
+++ b/LanMountainDesktop/Services/HostApplicationLifecycleService.cs
@@ -11,18 +11,21 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
{
public bool TryExit(HostApplicationLifecycleRequest? request = null)
{
+ App? app = null;
try
{
AppLogger.Info(
"HostLifecycle",
$"Exit requested. Source='{request?.Source ?? "Unknown"}'; Reason='{request?.Reason ?? string.Empty}'.");
- if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ app = Application.Current as App;
+ if (app?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
{
AppLogger.Warn("HostLifecycle", "Exit request ignored because desktop lifetime is unavailable.");
return false;
}
+ app.PrepareForShutdown(isRestart: false, request?.Source ?? "Unknown");
if (Dispatcher.UIThread.CheckAccess())
{
desktop.Shutdown();
@@ -36,6 +39,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
catch (Exception ex)
{
+ app?.ResetShutdownIntent(request?.Source ?? "Unknown");
AppLogger.Warn("HostLifecycle", "Failed to exit the application.", ex);
return false;
}
@@ -43,6 +47,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
public bool TryRestart(HostApplicationLifecycleRequest? request = null)
{
+ App? app = null;
try
{
var startInfo = AppRestartService.CreateRestartStartInfo();
@@ -55,6 +60,8 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
Process.Start(startInfo);
+ app = Application.Current as App;
+ app?.PrepareForShutdown(isRestart: true, request?.Source ?? "Unknown");
var exitRequest = request is null
? new HostApplicationLifecycleRequest(Reason: "Restart accepted.")
: request with
@@ -68,6 +75,7 @@ public sealed class HostApplicationLifecycleService : IHostApplicationLifecycle
}
catch (Exception ex)
{
+ app?.ResetShutdownIntent(request?.Source ?? "Unknown");
AppLogger.Warn("HostLifecycle", "Failed to restart the application.", ex);
return false;
}
diff --git a/LanMountainDesktop/Services/IndependentSettingsModuleService.cs b/LanMountainDesktop/Services/IndependentSettingsModuleService.cs
new file mode 100644
index 0000000..99927a9
--- /dev/null
+++ b/LanMountainDesktop/Services/IndependentSettingsModuleService.cs
@@ -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 ?? ""}'.");
+
+ 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 ?? ""}'.");
+ }
+ 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);
+ }
+}
diff --git a/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs b/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs
new file mode 100644
index 0000000..af3955b
--- /dev/null
+++ b/LanMountainDesktop/Views/IndependentSettingsModuleWindowBase.cs
@@ -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;
+ }
+ }
+}
diff --git a/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs b/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs
new file mode 100644
index 0000000..bfb126b
--- /dev/null
+++ b/LanMountainDesktop/Views/IndependentSettingsPageCategory.cs
@@ -0,0 +1,9 @@
+namespace LanMountainDesktop.Views;
+
+internal enum IndependentSettingsPageCategory
+{
+ Internal = 0,
+ External = 1,
+ About = 2,
+ Debug = 3
+}
diff --git a/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs b/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs
new file mode 100644
index 0000000..c8401cd
--- /dev/null
+++ b/LanMountainDesktop/Views/IndependentSettingsPageDefinition.cs
@@ -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);
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index 1253060..e7134d8 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -410,7 +410,10 @@ public partial class MainWindow
_reopenSettingsAfterComponentLibraryClose = false;
if (shouldReopenSettings)
{
- OpenSettingsPage();
+ if (Application.Current is App app)
+ {
+ app.OpenIndependentSettingsModule("ComponentLibrary");
+ }
}
}, FluttermotionToken.Slow);
}
diff --git a/LanMountainDesktop/Views/MainWindow.Settings.cs b/LanMountainDesktop/Views/MainWindow.Settings.cs
index 3193565..103f7ad 100644
--- a/LanMountainDesktop/Views/MainWindow.Settings.cs
+++ b/LanMountainDesktop/Views/MainWindow.Settings.cs
@@ -80,11 +80,13 @@ public partial class MainWindow
if (_isSettingsOpen)
{
- CloseSettingsPage();
- return;
+ CloseSettingsPage(immediate: true);
}
- OpenSettingsPage();
+ if (Application.Current is App app)
+ {
+ app.OpenIndependentSettingsModule("MainWindow");
+ }
}
private void OnCloseSettingsClick(object? sender, RoutedEventArgs e)
diff --git a/LanMountainDesktop/Views/SettingsComponentCategorySummary.cs b/LanMountainDesktop/Views/SettingsComponentCategorySummary.cs
new file mode 100644
index 0000000..aa1681f
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsComponentCategorySummary.cs
@@ -0,0 +1,3 @@
+namespace LanMountainDesktop.Views;
+
+internal sealed record SettingsComponentCategorySummary(string Name, string CountText);
diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
index 2075e2d..fe9534b 100644
--- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
@@ -1,4 +1,4 @@
-
-
+
-
-
+
+
-
+
-
+
@@ -49,17 +49,17 @@
Text="Current actual backend"
FontSize="12"
FontWeight="SemiBold"
- Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
+
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
new file mode 100644
index 0000000..1501242
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
new file mode 100644
index 0000000..cefb969
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+public partial class AppearanceSettingsPage : UserControl
+{
+ public AppearanceSettingsPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml
index 7cde1a8..c0835aa 100644
--- a/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/ColorSettingsPage.axaml
@@ -1,4 +1,4 @@
-
+
+
-
+
-
+
-
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
new file mode 100644
index 0000000..5d2c94c
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml
@@ -0,0 +1,48 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs
new file mode 100644
index 0000000..3a4ae8d
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/ComponentsSettingsPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+public partial class ComponentsSettingsPage : UserControl
+{
+ public ComponentsSettingsPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml
new file mode 100644
index 0000000..7102a9a
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs
new file mode 100644
index 0000000..dbd74e2
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/GeneralSettingsPage.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.Controls;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+public partial class GeneralSettingsPage : UserControl
+{
+ public GeneralSettingsPage()
+ {
+ InitializeComponent();
+ }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml
index 283b8dd..4cb7962 100644
--- a/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/GridSettingsPage.axaml
@@ -1,4 +1,4 @@
-
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}"
+ Margin="0,0,0,20"
+ Text="璋冩暣缃戞牸甯冨眬" />
+ HorizontalAlignment="Left">
+ Padding="8">
@@ -109,7 +111,7 @@
Width="80"
Minimum="6"
Maximum="96"
- Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="12" />
@@ -144,14 +146,14 @@
Width="80"
Minimum="0"
Maximum="30"
- Foreground="{DynamicResource AdaptiveTextPrimaryBrush}"
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}"
Value="18" />
@@ -161,12 +163,13 @@
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}"
+ Content="搴旂敤" />
+
diff --git a/LanMountainDesktop/Views/SettingsPages/LauncherSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/LauncherSettingsPage.axaml
index 5a3e962..209a8dd 100644
--- a/LanMountainDesktop/Views/SettingsPages/LauncherSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/LauncherSettingsPage.axaml
@@ -1,4 +1,4 @@
-
@@ -25,11 +25,11 @@
@@ -37,3 +37,4 @@
+
diff --git a/LanMountainDesktop/Views/SettingsPages/RegionSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/RegionSettingsPage.axaml
index e56e491..563bee3 100644
--- a/LanMountainDesktop/Views/SettingsPages/RegionSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/RegionSettingsPage.axaml
@@ -1,4 +1,4 @@
-
@@ -25,7 +25,7 @@
-
+
@@ -47,3 +47,4 @@
+
diff --git a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml
index 5eeb9a8..de42d86 100644
--- a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml
@@ -1,4 +1,4 @@
-
@@ -80,7 +80,7 @@
@@ -88,3 +88,4 @@
+
diff --git a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
index 074c6c2..4aec37e 100644
--- a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
@@ -1,4 +1,4 @@
-
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
@@ -65,7 +65,7 @@
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
@@ -110,13 +110,14 @@
IsVisible="False" />
+ Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
+ Foreground="{DynamicResource TextFillColorPrimaryBrush}" />
+
diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
index 1d24376..b782f96 100644
--- a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml
@@ -1,4 +1,4 @@
-
+ Margin="0,0,20,0"
+ Width="256"
+ MaxWidth="256"
+ VerticalAlignment="Top"
+ HorizontalAlignment="Left">
+ Padding="8">
-
+
-
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml
index 5fbd6ca..f662182 100644
--- a/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/WeatherSettingsPage.axaml
@@ -1,4 +1,4 @@
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
+ IsVisible="False"
+ Background="{DynamicResource SolidBackgroundFillColorBaseBrush}" />
-
-
+
+
-
-
-
-
+ Classes="independent-settings-titlebar"
+ PointerPressed="OnTitleBarPointerPressed"
+ DoubleTapped="OnTitleBarDoubleTapped">
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+ Text="Use stable left navigation and a single right-side page host, following the ClassIsland settings rhythm." />
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
+
+
-
+
+ Text="Configure this part of LanMountainDesktop in the independent settings module." />
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
@@ -324,4 +357,4 @@
-
+
diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
index 0ebdada..69c8783 100644
--- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
@@ -1,11 +1,14 @@
using System;
using System.Collections.Generic;
+using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
+using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
+using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Xaml;
@@ -22,12 +25,13 @@ using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
using LanMountainDesktop.Theme;
using LanMountainDesktop.Views.Components;
+using LanMountainDesktop.Views.SettingsPages;
using LibVLCSharp.Shared;
using Line = Avalonia.Controls.Shapes.Line;
namespace LanMountainDesktop.Views;
-public partial class SettingsWindow : Window
+public partial class SettingsWindow : IndependentSettingsModuleWindowBase
{
private enum WallpaperPlacement
{
@@ -102,8 +106,23 @@ public partial class SettingsWindow : Window
private readonly HashSet _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack _launcherFolderStack = [];
- private readonly Dictionary _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
- private readonly Dictionary _pluginSettingsNavItems = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _settingsNavItems = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _pluginSettingsNavItems = new(StringComparer.OrdinalIgnoreCase);
+ private readonly Dictionary _settingsPageDefinitions = new(StringComparer.OrdinalIgnoreCase);
+ private GeneralSettingsPage? GeneralSettingsHubPanel;
+ private AppearanceSettingsPage? AppearanceSettingsHubPanel;
+ private ComponentsSettingsPage? ComponentsSettingsHubPanel;
+ private WallpaperSettingsPage? WallpaperSettingsPanel;
+ private GridSettingsPage? GridSettingsPanel;
+ private ColorSettingsPage? ColorSettingsPanel;
+ private StatusBarSettingsPage? StatusBarSettingsPanel;
+ private WeatherSettingsPage? WeatherSettingsPanel;
+ private RegionSettingsPage? RegionSettingsPanel;
+ private UpdateSettingsPage? UpdateSettingsPanel;
+ private LauncherSettingsPage? LauncherSettingsPanel;
+ private AboutSettingsPage? AboutSettingsPanel;
+ private PluginSettingsPage? PluginSettingsPanel;
+ private PluginMarketSettingsPage? PluginMarketSettingsPanel;
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
@@ -168,20 +187,28 @@ public partial class SettingsWindow : Window
private bool _weatherNoTlsRequests;
private bool _autoStartWithWindows;
private string _weatherSearchKeyword = string.Empty;
- private string _selectedSettingsTabTag = "Wallpaper";
+ private string _selectedSettingsTabTag = "General";
+ private WallpaperPlacement _selectedWallpaperPlacement = WallpaperPlacement.Fill;
private bool _isWeatherSearchInProgress;
private bool _isWeatherPreviewInProgress;
+ private bool _controlsBound;
+ private bool _independentModuleInitializationCompleted;
+ private bool _suppressWallpaperPlacementEvents;
+ private bool _isIndependentSettingsModuleClosing;
+ private bool _allowIndependentSettingsModuleRealClose;
public SettingsWindow()
{
_componentRegistry = DesktopComponentRegistryFactory.Create((Application.Current as App)?.PluginRuntimeService);
InitializeComponent();
+ InitializeSettingsPageHosts();
InitializeSettingsNavigation();
InitializePluginSettingsNavigation();
_fluentAvaloniaTheme = Application.Current?.Styles.OfType().FirstOrDefault();
RequestedThemeVariant = Application.Current?.RequestedThemeVariant ?? ThemeVariant.Default;
PendingRestartStateService.StateChanged += OnPendingRestartStateChanged;
- HookEvents();
+ Closing += OnIndependentSettingsModuleClosing;
+ Opened += OnWindowOpened;
}
private void InitializeComponent()
@@ -235,7 +262,27 @@ public partial class SettingsWindow : Window
DownloadAndInstallUpdateButton.Click += OnDownloadAndInstallUpdateClick;
AutoStartWithWindowsToggleSwitch.IsCheckedChanged += OnAutoStartWithWindowsToggled;
AppRenderModeComboBox.SelectionChanged += OnAppRenderModeSelectionChanged;
- Opened += OnWindowOpened;
+ }
+
+ private void EnsureIndependentModuleControlsBound()
+ {
+ if (_controlsBound)
+ {
+ return;
+ }
+
+ AppLogger.Info("IndependentSettingsModule", "ControlsBindingStarted.");
+ try
+ {
+ HookEvents();
+ _controlsBound = true;
+ AppLogger.Info("IndependentSettingsModule", "ControlsBindingCompleted.");
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
+ {
+ AppLogger.Warn("IndependentSettingsModule", "ControlsBindingFailed.", ex);
+ throw new InvalidOperationException("Failed to bind independent settings module controls.", ex);
+ }
}
private void OnNightModeIsCheckedChanged(object? sender, RoutedEventArgs e)
@@ -273,69 +320,287 @@ public partial class SettingsWindow : Window
private void OnWindowOpened(object? sender, EventArgs e)
{
Opened -= OnWindowOpened;
- _suppressSettingsPersistence = true;
- var snapshot = _appSettingsService.Load();
- var launcherSnapshot = _launcherSettingsService.Load();
+ UpdateWindowChromeState();
+ UiExceptionGuard.FireAndForgetGuarded(
+ async () =>
+ {
+ EnsureIndependentModuleControlsBound();
+ await InitializeIndependentSettingsModuleAsync();
+ },
+ "IndependentSettingsModule.Initialize",
+ UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
+ ex =>
+ {
+ ShowIndependentModuleStatus(
+ L("settings.shell.init_failed_title", "设置模块初始化失败"),
+ ex.Message,
+ InfoBarSeverity.Warning);
+ return Task.CompletedTask;
+ });
+ }
- _targetShortSideCells = Math.Clamp(
- snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
- MinShortSideCells,
- MaxShortSideCells);
- _gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
- _desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
- _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
- _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
- GridSizeNumberBox.Value = _targetShortSideCells;
- GridSizeSlider.Value = _targetShortSideCells;
- GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
- GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
- GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
- StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
+ private void OnIndependentSettingsModuleClosing(object? sender, WindowClosingEventArgs e)
+ {
+ AppLogger.Info(
+ "IndependentSettingsModule",
+ $"CloseRequested; AllowRealClose={_allowIndependentSettingsModuleRealClose}; Reason='{e.CloseReason}'.");
+
+ if (!_allowIndependentSettingsModuleRealClose &&
+ e.CloseReason is not WindowCloseReason.ApplicationShutdown &&
+ e.CloseReason is not WindowCloseReason.OSShutdown)
{
- "Compact" => 0,
- "Custom" => 2,
- _ => 1
- };
- StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
- StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
- StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
- GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
- StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
- ApplyTaskbarSettings(snapshot);
- InitializeLocalization(snapshot.LanguageCode);
- InitializeWeatherSettings(snapshot);
- InitializeAutoStartWithWindowsSetting(snapshot);
- InitializeAppRenderModeSetting(snapshot);
- InitializeUpdateSettings(snapshot);
- InitializeLauncherVisibilitySettings(launcherSnapshot);
- InitializeSettingsIcons();
- ApplyLocalization();
- WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
- TryRestoreWallpaper(snapshot.WallpaperPath);
- RefreshColorPalettes();
- if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
- {
- _selectedThemeColor = savedThemeColor;
+ e.Cancel = true;
+ PersistSettings();
+ Hide();
+ AppLogger.Info("IndependentSettingsModule", "WindowHiddenByClose.");
+ return;
}
- _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
- ApplyNightModeState(_isNightMode, refreshPalettes: false);
- EnsureSelectedThemeColor();
- UpdateThemeColorSelectionState();
- ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
- _defaultDesktopBackground = DesktopWallpaperLayer.Background;
- RestoreSettingsTabSelection(snapshot);
- UpdateSettingsTabContent();
- UpdateWallpaperDisplay();
- UpdateWallpaperPreviewLayout();
- UpdateGridPreviewLayout();
- InitializeTimeZoneSettings();
- _ = LoadLauncherEntriesAsync();
- _suppressSettingsPersistence = false;
+ _isIndependentSettingsModuleClosing = true;
+ }
+
+ private async Task InitializeIndependentSettingsModuleAsync()
+ {
+ if (_independentModuleInitializationCompleted)
+ {
+ return;
+ }
+
+ AppLogger.Info("IndependentSettingsModule", "ModuleInitStarted; Stage='Opened'.");
+ _suppressSettingsPersistence = true;
+ try
+ {
+ ShowIndependentModuleStatus(string.Empty, string.Empty, InfoBarSeverity.Informational, isOpen: false);
+
+ var snapshot = new AppSettingsSnapshot();
+ var launcherSnapshot = new LauncherSettingsSnapshot();
+
+ await RunInitializationStageAsync("SnapshotLoad", () =>
+ {
+ snapshot = _appSettingsService.Load();
+ launcherSnapshot = _launcherSettingsService.Load();
+ return Task.CompletedTask;
+ });
+
+ await RunInitializationStageAsync("BaseConfiguration", () =>
+ {
+ _targetShortSideCells = Math.Clamp(
+ snapshot.GridShortSideCells > 0 ? snapshot.GridShortSideCells : CalculateDefaultShortSideCellCountFromDpi(),
+ MinShortSideCells,
+ MaxShortSideCells);
+ _gridSpacingPreset = NormalizeGridSpacingPreset(snapshot.GridSpacingPreset);
+ _desktopEdgeInsetPercent = Math.Clamp(snapshot.DesktopEdgeInsetPercent, MinEdgeInsetPercent, MaxEdgeInsetPercent);
+ _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode);
+ _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30);
+ GridSizeNumberBox.Value = _targetShortSideCells;
+ GridSizeSlider.Value = _targetShortSideCells;
+ GridSpacingPresetComboBox.SelectedIndex = string.Equals(_gridSpacingPreset, "Compact", StringComparison.OrdinalIgnoreCase) ? 1 : 0;
+ GridEdgeInsetSlider.Value = _desktopEdgeInsetPercent;
+ GridEdgeInsetNumberBox.Value = _desktopEdgeInsetPercent;
+ StatusBarSpacingModeComboBox.SelectedIndex = _statusBarSpacingMode switch
+ {
+ "Compact" => 0,
+ "Custom" => 2,
+ _ => 1
+ };
+ StatusBarSpacingSlider.Value = _statusBarCustomSpacingPercent;
+ StatusBarSpacingNumberBox.Value = _statusBarCustomSpacingPercent;
+ StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase);
+ GridEdgeInsetNumberBox.ValueChanged += OnGridEdgeInsetNumberBoxChanged;
+ StatusBarSpacingNumberBox.ValueChanged += OnStatusBarSpacingNumberBoxChanged;
+ ApplyTaskbarSettings(snapshot);
+ InitializeLocalization(snapshot.LanguageCode);
+ InitializeWeatherSettings(snapshot);
+ InitializeAutoStartWithWindowsSetting(snapshot);
+ InitializeAppRenderModeSetting(snapshot);
+ InitializeUpdateSettings(snapshot);
+ InitializeLauncherVisibilitySettings(launcherSnapshot);
+ InitializeSettingsIcons();
+ ApplyLocalization();
+ return Task.CompletedTask;
+ });
+
+ await RunInitializationStageAsync("VisualState", () =>
+ {
+ _selectedWallpaperPlacement = GetWallpaperPlacementFromIndex(GetPlacementIndexFromSetting(snapshot.WallpaperPlacement));
+ _suppressWallpaperPlacementEvents = true;
+ WallpaperPlacementComboBox.SelectedIndex = GetPlacementIndexFromSetting(snapshot.WallpaperPlacement);
+ _suppressWallpaperPlacementEvents = false;
+ TryRestoreWallpaper(snapshot.WallpaperPath);
+ RefreshColorPalettes();
+ if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor))
+ {
+ _selectedThemeColor = savedThemeColor;
+ }
+
+ _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold);
+ ApplyNightModeState(_isNightMode, refreshPalettes: false);
+ EnsureSelectedThemeColor();
+ UpdateThemeColorSelectionState();
+ ThemeColorStatusTextBlock.Text = Lf("settings.color.theme_ready_format", "Theme color ready: {0}.", _selectedThemeColor);
+ _defaultDesktopBackground = DesktopWallpaperLayer.Background;
+ RestoreSettingsTabSelection(snapshot);
+ UpdateSettingsTabContent();
+ UpdateWallpaperDisplay();
+ UpdateWallpaperPreviewLayout();
+ UpdateGridPreviewLayout();
+ InitializeTimeZoneSettings();
+ return Task.CompletedTask;
+ });
+
+ UiExceptionGuard.FireAndForgetGuarded(
+ LoadLauncherEntriesAsync,
+ "IndependentSettingsModule.LoadLauncherEntries",
+ UiExceptionGuard.BuildContext(("Window", nameof(SettingsWindow))),
+ ex =>
+ {
+ ShowIndependentModuleStatus(
+ L("settings.shell.partial_warning_title", "部分内容未能载入"),
+ ex.Message,
+ InfoBarSeverity.Warning);
+ return Task.CompletedTask;
+ });
+
+ _independentModuleInitializationCompleted = true;
+ AppLogger.Info("IndependentSettingsModule", "ModuleInitCompleted.");
+ }
+ finally
+ {
+ _suppressSettingsPersistence = false;
+ }
+ }
+
+ private async Task RunInitializationStageAsync(string stage, Func action)
+ {
+ AppLogger.Info("IndependentSettingsModule", $"ModuleInitStarted; Stage='{stage}'.");
+ try
+ {
+ await action();
+ AppLogger.Info("IndependentSettingsModule", $"ModuleInitCompleted; Stage='{stage}'.");
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
+ {
+ AppLogger.Warn("IndependentSettingsModule", $"ModuleInitFailed; Stage='{stage}'.", ex);
+ ShowIndependentModuleStatus(
+ L("settings.shell.partial_warning_title", "部分内容未能载入"),
+ ex.Message,
+ InfoBarSeverity.Warning);
+ }
+ }
+
+ private void ShowIndependentModuleStatus(string title, string message, InfoBarSeverity severity, bool isOpen = true)
+ {
+ if (IndependentSettingsStatusInfoBar is null)
+ {
+ return;
+ }
+
+ IndependentSettingsStatusInfoBar.Title = title;
+ IndependentSettingsStatusInfoBar.Message = message;
+ IndependentSettingsStatusInfoBar.Severity = severity;
+ IndependentSettingsStatusInfoBar.IsOpen = isOpen;
+ }
+
+ private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ {
+ BeginMoveDrag(e);
+ }
+ }
+
+ private void OnTitleBarDoubleTapped(object? sender, RoutedEventArgs e)
+ {
+ if (!CanResize)
+ {
+ return;
+ }
+
+ WindowState = WindowState == WindowState.Maximized
+ ? WindowState.Normal
+ : WindowState.Maximized;
+ UpdateWindowChromeState();
+ }
+
+ private void OnMinimizeWindowClick(object? sender, RoutedEventArgs e)
+ {
+ WindowState = WindowState.Minimized;
+ UpdateWindowChromeState();
+ }
+
+ private void OnToggleWindowStateClick(object? sender, RoutedEventArgs e)
+ {
+ if (!CanResize)
+ {
+ return;
+ }
+
+ WindowState = WindowState == WindowState.Maximized
+ ? WindowState.Normal
+ : WindowState.Maximized;
+ UpdateWindowChromeState();
+ }
+
+ private void UpdateWindowChromeState()
+ {
+ if (WindowStateToggleIcon is null)
+ {
+ return;
+ }
+
+ WindowStateToggleIcon.Symbol = WindowState == WindowState.Maximized
+ ? FluentIcons.Common.Symbol.SquareMultiple
+ : FluentIcons.Common.Symbol.Square;
}
private void OnCloseWindowClick(object? sender, RoutedEventArgs e)
{
Close();
}
+
+ private void OnSettingsPaneToggleButtonClick(object? sender, RoutedEventArgs e)
+ {
+ if (SettingsNavView is not null)
+ {
+ SettingsNavView.IsPaneOpen = !SettingsNavView.IsPaneOpen;
+ }
+ }
+
+ private void OnOpenLogsFolderClick(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = AppLogger.LogDirectory,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
+ {
+ ShowIndependentModuleStatus(
+ L("settings.shell.partial_warning_title", "部分内容未能加载"),
+ ex.Message,
+ InfoBarSeverity.Warning);
+ }
+ }
+
+ private void OnOpenAppFolderClick(object? sender, RoutedEventArgs e)
+ {
+ try
+ {
+ Process.Start(new ProcessStartInfo
+ {
+ FileName = Path.GetFullPath(".") ?? string.Empty,
+ UseShellExecute = true
+ });
+ }
+ catch (Exception ex) when (!UiExceptionGuard.IsFatalException(ex))
+ {
+ ShowIndependentModuleStatus(
+ L("settings.shell.partial_warning_title", "部分内容未能加载"),
+ ex.Message,
+ InfoBarSeverity.Warning);
+ }
+ }
}
diff --git a/LanMountainDesktop/plugins/PluginLoader.cs b/LanMountainDesktop/plugins/PluginLoader.cs
index 847ebf9..13f4ee8 100644
--- a/LanMountainDesktop/plugins/PluginLoader.cs
+++ b/LanMountainDesktop/plugins/PluginLoader.cs
@@ -145,22 +145,29 @@ public sealed class PluginLoader
{
Directory.CreateDirectory(dataDirectory);
ValidatePluginRuntimeAssets(manifest, assemblyPath, pluginDirectory);
+ AppLogger.Info(
+ "PluginLoader",
+ $"LoadCore starting. PluginId='{manifest.Id}'; AssemblyPath='{assemblyPath}'; PluginDirectory='{pluginDirectory}'; DataDirectory='{dataDirectory}'.");
loadContext = new PluginLoadContext(assemblyPath, _options.SharedAssemblyNames);
var assembly = loadContext.LoadFromAssemblyPath(assemblyPath);
+ AppLogger.Info("PluginLoader", $"Assembly loaded. PluginId='{manifest.Id}'; Assembly='{assembly.FullName}'.");
var pluginType = ResolvePluginType(assembly);
plugin = CreatePluginInstance(pluginType);
+ AppLogger.Info("PluginLoader", $"Plugin instance created. PluginId='{manifest.Id}'; PluginType='{pluginType.FullName}'.");
runtimeContext = CreateRuntimeContext(manifest, pluginDirectory, dataDirectory, properties);
var serviceCollection = CreateServiceCollection(runtimeContext, services);
var hostBuilderContext = CreateHostBuilderContext(runtimeContext);
plugin.Initialize(hostBuilderContext, serviceCollection);
+ AppLogger.Info("PluginLoader", $"Plugin Initialize completed. PluginId='{manifest.Id}'.");
pluginServices = serviceCollection.BuildServiceProvider(new ServiceProviderOptions
{
ValidateScopes = false,
ValidateOnBuild = true
});
+ AppLogger.Info("PluginLoader", $"Service provider built. PluginId='{manifest.Id}'.");
runtimeContext.SetServices(pluginServices);
var settingsPages = pluginServices
@@ -174,8 +181,12 @@ public sealed class PluginLoader
.ThenBy(component => component.DisplayName, StringComparer.OrdinalIgnoreCase)
.ToArray();
var exportedServices = ResolveExports(manifest, pluginServices);
+ AppLogger.Info(
+ "PluginLoader",
+ $"Plugin contributions resolved. PluginId='{manifest.Id}'; SettingsPages={settingsPages.Length}; Widgets={desktopComponents.Length}; Exports={exportedServices.Count}.");
hostedServices = pluginServices.GetServices().ToArray();
StartHostedServices(hostedServices);
+ AppLogger.Info("PluginLoader", $"Hosted services started. PluginId='{manifest.Id}'; HostedServices={hostedServices.Count}.");
var loadedPlugin = new LoadedPlugin(
manifest,
@@ -375,6 +386,7 @@ public sealed class PluginLoader
{
foreach (var hostedService in hostedServices)
{
+ AppLogger.Info("PluginLoader", $"Starting hosted service '{hostedService.GetType().FullName}'.");
hostedService.StartAsync(CancellationToken.None).GetAwaiter().GetResult();
}
}
diff --git a/LanMountainDesktop/plugins/PluginMarketIndexService.cs b/LanMountainDesktop/plugins/PluginMarketIndexService.cs
index a7e5ffb..75630dd 100644
--- a/LanMountainDesktop/plugins/PluginMarketIndexService.cs
+++ b/LanMountainDesktop/plugins/PluginMarketIndexService.cs
@@ -32,7 +32,7 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
try
{
- var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken);
+ var json = await File.ReadAllTextAsync(localIndexPath, cancellationToken).ConfigureAwait(false);
var document = AirAppMarketIndexDocument.Load(json, localIndexPath);
_cacheService.SaveIndexJson(json);
return new AirAppMarketLoadResult(
@@ -61,8 +61,8 @@ internal sealed class AirAppMarketIndexService : IDisposable
{
using var response = await _httpClient.GetAsync(
AirAppMarketDefaults.DefaultIndexUrl,
- cancellationToken);
- var json = await response.Content.ReadAsStringAsync(cancellationToken);
+ cancellationToken).ConfigureAwait(false);
+ var json = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
var document = AirAppMarketIndexDocument.Load(json, AirAppMarketDefaults.DefaultIndexUrl);
diff --git a/LanMountainDesktop/plugins/PluginMarketModels.cs b/LanMountainDesktop/plugins/PluginMarketModels.cs
index f018bba..dce923c 100644
--- a/LanMountainDesktop/plugins/PluginMarketModels.cs
+++ b/LanMountainDesktop/plugins/PluginMarketModels.cs
@@ -31,14 +31,8 @@ internal static class AirAppMarketDefaults
public static string? TryGetWorkspaceIndexPath()
{
- var repositoryRoot = TryGetWorkspaceRepositoryRoot("LanAirApp");
- if (repositoryRoot is null)
- {
- return null;
- }
-
- var candidatePath = Path.Combine(repositoryRoot, "airappmarket", "index.json");
- return File.Exists(candidatePath) ? candidatePath : null;
+ var relativePath = Path.Combine("airappmarket", "index.json");
+ return TryResolveWorkspacePath("LanAirApp", relativePath);
}
public static bool TryResolveWorkspaceFile(string url, out string localPath)
@@ -57,14 +51,8 @@ internal static class AirAppMarketDefaults
return false;
}
- var repositoryRoot = TryGetWorkspaceRepositoryRoot(repositoryName);
- if (repositoryRoot is null)
- {
- return false;
- }
-
- var candidatePath = Path.GetFullPath(Path.Combine(repositoryRoot, relativePath));
- if (!File.Exists(candidatePath))
+ var candidatePath = TryResolveWorkspacePath(repositoryName, relativePath);
+ if (candidatePath is null)
{
return false;
}
@@ -99,7 +87,7 @@ internal static class AirAppMarketDefaults
return !string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repositoryName);
}
- private static string? TryGetWorkspaceRepositoryRoot(string repositoryName)
+ private static string? TryResolveWorkspacePath(string repositoryName, string relativePath)
{
var current = new DirectoryInfo(AppContext.BaseDirectory);
while (current is not null)
@@ -107,7 +95,11 @@ internal static class AirAppMarketDefaults
var candidate = Path.Combine(current.FullName, repositoryName);
if (Directory.Exists(candidate))
{
- return candidate;
+ var candidatePath = Path.GetFullPath(Path.Combine(candidate, relativePath));
+ if (File.Exists(candidatePath))
+ {
+ return candidatePath;
+ }
}
current = current.Parent;
diff --git a/LanMountainDesktop/plugins/PluginMarketSettingsPage.axaml b/LanMountainDesktop/plugins/PluginMarketSettingsPage.axaml
index 6f97bb1..c9e73ec 100644
--- a/LanMountainDesktop/plugins/PluginMarketSettingsPage.axaml
+++ b/LanMountainDesktop/plugins/PluginMarketSettingsPage.axaml
@@ -1,25 +1,43 @@
-
+ Text="Browse plugins from the official LanAirApp source, review package details, and stage installations safely." />
-
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/plugins/PluginRuntimeService.cs b/LanMountainDesktop/plugins/PluginRuntimeService.cs
index c5b52f0..438f727 100644
--- a/LanMountainDesktop/plugins/PluginRuntimeService.cs
+++ b/LanMountainDesktop/plugins/PluginRuntimeService.cs
@@ -66,6 +66,7 @@ public sealed class PluginRuntimeService : IDisposable
Directory.CreateDirectory(PluginsDirectory);
ApplyPendingPluginDeletions();
UnloadInstalledPlugins();
+ AppLogger.Info("PluginRuntime", $"Loading installed plugins from '{PluginsDirectory}'.");
var disabledPluginIds = GetDisabledPluginIds();
var settingsSnapshot = _appSettingsService.Load();
@@ -81,6 +82,9 @@ public sealed class PluginRuntimeService : IDisposable
var discoveryFailures = new List();
var candidates = DiscoverCandidates(discoveryFailures);
_loadResults.AddRange(discoveryFailures);
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Plugin discovery completed. Candidates={candidates.Count}; DiscoveryFailures={discoveryFailures.Count}; PluginsDirectory='{PluginsDirectory}'.");
var selectedPluginIds = new HashSet(StringComparer.OrdinalIgnoreCase);
foreach (var candidate in candidates)
@@ -93,6 +97,7 @@ public sealed class PluginRuntimeService : IDisposable
new InvalidOperationException(
$"Duplicate plugin id '{candidate.Manifest.Id}' was found. Source '{candidate.SourcePath}' was ignored because a higher-priority source was already selected."));
_loadResults.Add(duplicateFailure);
+ LogPluginFailure("CatalogSelection", duplicateFailure, treatAsError: false);
continue;
}
@@ -113,7 +118,13 @@ public sealed class PluginRuntimeService : IDisposable
try
{
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Preparing shared contracts. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
RegisterSharedContractsForLoad(candidate.Manifest);
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Shared contracts ready. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'.");
}
catch (Exception ex)
{
@@ -128,10 +139,13 @@ public sealed class PluginRuntimeService : IDisposable
ex.Message,
0,
0));
- Debug.WriteLine($"[PluginRuntime] Failed to prepare dependencies for '{candidate.Manifest.Id}': {ex}");
+ LogPluginFailure("DependencyPrepare", dependencyFailure, treatAsError: false);
continue;
}
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Starting plugin load. PluginId='{candidate.Manifest.Id}'; SourcePath='{candidate.SourcePath}'; SourceKind='{candidate.SourceKind}'.");
var loadResult = candidate.SourceKind switch
{
PluginCatalogSourceKind.Package => _loader.LoadFromPackage(
@@ -160,6 +174,9 @@ public sealed class PluginRuntimeService : IDisposable
null,
loadResult.LoadedPlugin.SettingsPages.Count,
loadResult.LoadedPlugin.DesktopComponents.Count));
+ AppLogger.Info(
+ "PluginRuntime",
+ $"Plugin loaded. PluginId='{loadResult.LoadedPlugin.Manifest.Id}'; SourcePath='{loadResult.SourcePath}'; ManifestVersion='{loadResult.LoadedPlugin.Manifest.Version ?? ""}'; ApiVersion='{loadResult.LoadedPlugin.Manifest.ApiVersion ?? ""}'; SourceKind='{candidate.SourceKind}'; SettingsPages={loadResult.LoadedPlugin.SettingsPages.Count}; Widgets={loadResult.LoadedPlugin.DesktopComponents.Count}.");
Debug.WriteLine($"[PluginRuntime] Loaded '{loadResult.Manifest?.Id}' from '{loadResult.SourcePath}'.");
continue;
}
@@ -173,11 +190,15 @@ public sealed class PluginRuntimeService : IDisposable
loadResult.Error?.Message,
0,
0));
+ LogPluginFailure("Load", loadResult, treatAsError: true);
Debug.WriteLine($"[PluginRuntime] Failed to load plugin from '{loadResult.SourcePath}': {loadResult.Error}");
}
if (_catalog.Count == 0 && discoveryFailures.Count == 0)
{
+ AppLogger.Info(
+ "PluginRuntime",
+ $"No plugin packages or loose manifests were discovered under '{PluginsDirectory}'.");
Debug.WriteLine($"[PluginRuntime] No .laapp packages or loose plugin manifests found under '{PluginsDirectory}'.");
}
}
@@ -392,7 +413,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
- failures.Add(PluginLoadResult.Failure(packagePath, null, ex));
+ var failure = PluginLoadResult.Failure(packagePath, null, ex);
+ failures.Add(failure);
+ LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -405,7 +428,9 @@ public sealed class PluginRuntimeService : IDisposable
}
catch (Exception ex)
{
- failures.Add(PluginLoadResult.Failure(manifestPath, null, ex));
+ var failure = PluginLoadResult.Failure(manifestPath, null, ex);
+ failures.Add(failure);
+ LogPluginFailure("ManifestValidation", failure, treatAsError: false);
}
}
@@ -717,6 +742,21 @@ public sealed class PluginRuntimeService : IDisposable
return Path.Combine(PluginsDirectory, PendingDeletionFileName);
}
+ private static void LogPluginFailure(string stage, PluginLoadResult result, bool treatAsError)
+ {
+ var manifest = result.Manifest;
+ var message =
+ $"Plugin load issue. Stage='{stage}'; PluginId='{manifest?.Id ?? ""}'; SourcePath='{result.SourcePath}'; ManifestVersion='{manifest?.Version ?? ""}'; ApiVersion='{manifest?.ApiVersion ?? ""}'; Error='{result.Error?.Message ?? ""}'.";
+
+ if (treatAsError)
+ {
+ AppLogger.Error("PluginRuntime", message, result.Error);
+ return;
+ }
+
+ AppLogger.Warn("PluginRuntime", message, result.Error);
+ }
+
private void RemovePluginFromSnapshot(string pluginId)
{
var snapshot = _appSettingsService.Load();
diff --git a/LanMountainDesktop/plugins/PluginSettingsPage.axaml b/LanMountainDesktop/plugins/PluginSettingsPage.axaml
index caefe78..2e05aa9 100644
--- a/LanMountainDesktop/plugins/PluginSettingsPage.axaml
+++ b/LanMountainDesktop/plugins/PluginSettingsPage.axaml
@@ -7,31 +7,40 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="1000"
x:Class="LanMountainDesktop.Views.SettingsPages.PluginSettingsPage">
-
+
+
+
-
+
-
@@ -47,16 +56,17 @@
Description="Manage installed plugins here."
IsExpanded="True">
-
+
@@ -70,7 +80,8 @@
Description="Open a .laapp package and stage it into the local plugin directory."
IsExpanded="False">
-
+
@@ -79,11 +90,11 @@
Click="OnInstallPluginPackageClick"
Content="Open .laapp package" />
diff --git a/LanMountainDesktop/plugins/PluginSharedContractManager.cs b/LanMountainDesktop/plugins/PluginSharedContractManager.cs
index 36de8e1..673bed2 100644
--- a/LanMountainDesktop/plugins/PluginSharedContractManager.cs
+++ b/LanMountainDesktop/plugins/PluginSharedContractManager.cs
@@ -8,6 +8,7 @@ using System.Runtime.Loader;
using System.Security.Cryptography;
using System.Threading;
using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services;
using LanMountainDesktop.Views.SettingsPages;
namespace LanMountainDesktop.Plugins;
@@ -48,6 +49,9 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var document = LoadIndex(cancellationToken);
+ AppLogger.Info(
+ "PluginSharedContracts",
+ $"Shared contract index loaded for plugin '{manifest.Id}'. SourceContracts={document.Contracts.Count}.");
foreach (var reference in manifest.SharedContracts)
{
EnsureInstalled(document, reference, cancellationToken);
@@ -64,9 +68,19 @@ internal sealed class PluginSharedContractManager : IDisposable
}
var assemblyNames = new List(manifest.SharedContracts.Count);
+ AirAppMarketIndexDocument? document = null;
foreach (var reference in manifest.SharedContracts)
{
var assemblyPath = GetInstalledAssemblyPath(reference);
+ if (!File.Exists(assemblyPath))
+ {
+ document ??= LoadIndex(cancellationToken);
+ AppLogger.Info(
+ "PluginSharedContracts",
+ $"Installing missing shared contract during plugin load. PluginId='{manifest.Id}'; ContractId='{reference.Id}'; Version='{reference.Version}'; Destination='{assemblyPath}'.");
+ EnsureInstalled(document, reference, cancellationToken);
+ }
+
if (!File.Exists(assemblyPath))
{
throw new InvalidOperationException(
@@ -115,10 +129,12 @@ internal sealed class PluginSharedContractManager : IDisposable
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
var temporaryPath = destinationPath + ".download";
+ var resolvedSource = entry.DownloadUrl;
try
{
if (AirAppMarketDefaults.TryResolveWorkspaceFile(entry.DownloadUrl, out var localSourcePath))
{
+ resolvedSource = localSourcePath;
File.Copy(localSourcePath, temporaryPath, overwrite: true);
}
else
@@ -136,6 +152,9 @@ internal sealed class PluginSharedContractManager : IDisposable
ValidateInstalledFile(temporaryPath, entry);
File.Move(temporaryPath, destinationPath, overwrite: true);
+ AppLogger.Info(
+ "PluginSharedContracts",
+ $"Installed shared contract. ContractId='{reference.Id}'; Version='{reference.Version}'; Source='{resolvedSource}'; Destination='{destinationPath}'.");
}
finally
{
@@ -145,6 +164,7 @@ internal sealed class PluginSharedContractManager : IDisposable
private AirAppMarketIndexDocument LoadIndex(CancellationToken cancellationToken)
{
+ AppLogger.Info("PluginSharedContracts", "Loading market index for shared contract resolution.");
var result = _indexService.LoadAsync(cancellationToken).GetAwaiter().GetResult();
if (!result.Success || result.Document is null)
{
@@ -152,6 +172,10 @@ internal sealed class PluginSharedContractManager : IDisposable
$"Failed to load market index for shared contract resolution: {result.ErrorMessage ?? "Unknown error"}");
}
+ AppLogger.Info(
+ "PluginSharedContracts",
+ $"Market index ready. Source='{result.Source}'; Location='{result.SourceLocation}'; Warning='{result.WarningMessage ?? string.Empty}'.");
+
return result.Document;
}
diff --git a/LanMountainDesktop/plugins/SettingsWindow.PluginSettingsHost.cs b/LanMountainDesktop/plugins/SettingsWindow.PluginSettingsHost.cs
index ed1d40a..8ac632e 100644
--- a/LanMountainDesktop/plugins/SettingsWindow.PluginSettingsHost.cs
+++ b/LanMountainDesktop/plugins/SettingsWindow.PluginSettingsHost.cs
@@ -3,9 +3,9 @@ using System.Collections.Generic;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Layout;
using Avalonia.Media;
using FluentIcons.Common;
+using FluentAvalonia.UI.Controls;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
@@ -17,11 +17,12 @@ public partial class SettingsWindow
private void InitializePluginSettingsNavigation()
{
- if (_pluginSettingsPageHosts.Count > 0)
- {
- return;
- }
+ _pluginSettingsPageHosts.Clear();
+ _pluginSettingsNavItems.Clear();
+ }
+ private void RegisterPluginSettingsDefinitions()
+ {
var runtime = (Application.Current as App)?.PluginRuntimeService;
var contributions = runtime?.SettingsPages
.OrderBy(contribution => contribution.Registration.SortOrder)
@@ -31,7 +32,6 @@ public partial class SettingsWindow
if (contributions is not { Length: > 0 })
{
- SettingsPluginNavSection.IsVisible = false;
return;
}
@@ -39,23 +39,21 @@ public partial class SettingsWindow
.GroupBy(contribution => contribution.Plugin.Manifest.Id, StringComparer.OrdinalIgnoreCase)
.ToDictionary(group => group.Key, group => group.Count(), StringComparer.OrdinalIgnoreCase);
- foreach (var contribution in contributions)
+ for (var i = 0; i < contributions.Length; i++)
{
+ var contribution = contributions[i];
var tag = BuildPluginSettingsTag(contribution);
- var navigationTitle = BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId);
- var navItem = CreateSettingsNavItem(tag, Symbol.PuzzlePiece, navigationTitle);
- ToolTip.SetTip(navItem, $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}");
+ _pluginSettingsPageHosts[tag] = CreatePluginSettingsPageHost(contribution);
- SettingsPluginNavHost.Children.Add(navItem);
- _pluginSettingsNavItems[tag] = navItem;
-
- var pageHost = CreatePluginSettingsPageHost(contribution);
- pageHost.IsVisible = false;
- SettingsContentPagesHost.Children.Add(pageHost);
- _pluginSettingsPageHosts[tag] = pageHost;
+ RegisterSettingsPageDefinition(new IndependentSettingsPageDefinition(
+ tag,
+ BuildPluginSettingsNavigationTitle(contribution, pageCountsByPluginId),
+ BuildPluginSettingsPageDescription(contribution),
+ FluentIcons.Common.Symbol.PuzzlePiece,
+ IndependentSettingsPageCategory.External,
+ 200 + i,
+ $"{contribution.Plugin.Manifest.Name} - {contribution.Registration.Title}"));
}
-
- SettingsPluginNavSection.IsVisible = SettingsPluginNavHost.Children.Count > 0;
}
private static string BuildPluginSettingsTag(PluginSettingsPageContribution contribution)
@@ -72,6 +70,15 @@ public partial class SettingsWindow
: contribution.Plugin.Manifest.Name;
}
+ private string BuildPluginSettingsPageDescription(PluginSettingsPageContribution contribution)
+ {
+ return Lf(
+ "settings.page_desc.plugin_contributed_format",
+ "Settings page '{0}' is provided by plugin '{1}'.",
+ contribution.Registration.Title,
+ contribution.Plugin.Manifest.Name);
+ }
+
private Control CreatePluginSettingsPageHost(PluginSettingsPageContribution contribution)
{
Control content;
@@ -87,6 +94,7 @@ public partial class SettingsWindow
return new StackPanel
{
Spacing = 16,
+ MaxWidth = 920,
Children =
{
new TextBlock
@@ -94,12 +102,12 @@ public partial class SettingsWindow
Text = contribution.Registration.Title,
FontSize = 24,
FontWeight = FontWeight.SemiBold,
- Foreground = GetThemeBrush("AdaptiveTextPrimaryBrush")
+ Foreground = GetThemeBrush("TextFillColorPrimaryBrush")
},
new TextBlock
{
Text = contribution.Plugin.Manifest.Name,
- Foreground = GetThemeBrush("AdaptiveTextSecondaryBrush")
+ Foreground = GetThemeBrush("TextFillColorSecondaryBrush")
},
content
}
@@ -123,58 +131,32 @@ public partial class SettingsWindow
};
}
- private void UpdatePluginSettingsPageVisibility(string? selectedTag)
- {
- foreach (var pair in _pluginSettingsPageHosts)
- {
- pair.Value.IsVisible = string.Equals(pair.Key, selectedTag, StringComparison.OrdinalIgnoreCase);
- }
- }
-
internal void RefreshPluginSettingsNavigation()
{
- foreach (var pair in _pluginSettingsPageHosts.ToArray())
- {
- if (_pluginSettingsNavItems.TryGetValue(pair.Key, out var navItem))
- {
- SettingsPluginNavHost.Children.Remove(navItem);
- }
-
- SettingsContentPagesHost.Children.Remove(pair.Value);
- }
-
- _pluginSettingsPageHosts.Clear();
- _pluginSettingsNavItems.Clear();
- SettingsPluginNavSection.IsVisible = false;
- InitializePluginSettingsNavigation();
-
- if (GetSettingsNavItem(_selectedSettingsTabTag) is null)
- {
- SelectSettingsTab("Plugins", persistSelection: false);
- }
- else
- {
- SelectSettingsTab(_selectedSettingsTabTag, persistSelection: false);
- }
+ var preferredTag = NormalizeSettingsPageTag(_selectedSettingsTabTag);
+ InitializeSettingsNavigation();
+ SelectSettingsTab(
+ _settingsPageDefinitions.ContainsKey(preferredTag) ? preferredTag : "Plugins",
+ persistSelection: false);
+ PluginSettingsPanel?.RefreshFromRuntime();
}
private string? GetSelectedSettingsTabTag()
{
- return _selectedSettingsTabTag;
+ return NormalizeSettingsPageTag(_selectedSettingsTabTag);
}
private int ResolveSelectedSettingsTabIndex()
{
- var selectedTag = GetSelectedSettingsTabTag();
- if (string.IsNullOrWhiteSpace(selectedTag))
+ if (SettingsNavView?.MenuItems is null)
{
return 0;
}
- var buttons = EnumerateSettingsNavItems().ToList();
- for (var i = 0; i < buttons.Count; i++)
+ var items = SettingsNavView.MenuItems.OfType().ToList();
+ for (var i = 0; i < items.Count; i++)
{
- if (string.Equals(buttons[i].Tag?.ToString(), selectedTag, StringComparison.OrdinalIgnoreCase))
+ if (string.Equals(items[i].Tag?.ToString(), NormalizeSettingsPageTag(_selectedSettingsTabTag), StringComparison.OrdinalIgnoreCase))
{
return i;
}
@@ -185,21 +167,32 @@ public partial class SettingsWindow
private void RestoreSettingsTabSelection(AppSettingsSnapshot snapshot)
{
- var buttons = EnumerateSettingsNavItems().ToList();
- if (buttons.Count == 0)
+ if (SettingsNavView?.MenuItems is null || SettingsNavView.MenuItems.Count == 0)
{
return;
}
- if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag) &&
- GetSettingsNavItem(snapshot.SettingsTabTag) is not null)
+ var items = SettingsNavView.MenuItems.OfType().ToList();
+ if (items.Count == 0)
{
- SelectSettingsTab(snapshot.SettingsTabTag, persistSelection: false);
return;
}
- var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, buttons.Count - 1));
- var button = buttons[safeIndex];
- SelectSettingsTab(button.Tag?.ToString() ?? "Wallpaper", persistSelection: false);
+ if (!string.IsNullOrWhiteSpace(snapshot.SettingsTabTag))
+ {
+ var normalizedTag = NormalizeSettingsPageTag(snapshot.SettingsTabTag);
+ var taggedItem = items
+ .FirstOrDefault(item => string.Equals(item.Tag?.ToString(), normalizedTag, StringComparison.OrdinalIgnoreCase));
+ if (taggedItem is not null)
+ {
+ _selectedSettingsTabTag = normalizedTag;
+ SettingsNavView.SelectedItem = taggedItem;
+ return;
+ }
+ }
+
+ var safeIndex = Math.Clamp(snapshot.SettingsTabIndex, 0, Math.Max(0, items.Count - 1));
+ _selectedSettingsTabTag = items[safeIndex].Tag?.ToString() ?? _selectedSettingsTabTag;
+ SettingsNavView.SelectedItem = items[safeIndex];
}
}