Launcher fix (#6)

* fix.hy3试图修复中

* Resolve dev paths and fix splash UI thread

Compute a solutionRoot and expand development search paths (LanMountainDesktop and dev-test) in DeploymentLocator, add logging when scanning/finding hosts, and return distinct full paths. Ensure backward-compatible path checks. Fix cross-thread UI calls: invoke splashWindow.DismissAsync on the UI thread in LauncherFlowCoordinator, and make SplashWindow.DismissAsync ensure it runs on the UI thread before closing (simplified Close call). These changes improve development host discovery and prevent UI-thread access issues during shutdown.

* Add configurable data location (portable/system)

Introduce support for choosing and resolving the application's data root (system user dir vs. portable app folder). Adds DataLocationConfig model, DataLocationResolver (load/save/resolve/migrate), a UI prompt (DataLocationPromptWindow) and an OOBE step (DataLocationOobeStep) to let users pick and optionally migrate existing data. Wire the chosen data root into the launcher flow and host launch plan (forwarded via --data-root and LMD_DATA_ROOT), and add AppDataPathProvider to let runtime services read the effective data root (initialized in Program.Main). Update various services (logging, settings, DB, plugin/market, startup registry, etc.) to use the new provider/resolver and register the config type in the JSON context. This enables portable installs, safe migration, and runtime overrides via CLI or environment variable.

* Add dev/debug startup flow and launch profiles

Handle design-time initialization and add a developer debug startup path: App now skips normal startup when in design mode and shows a DevDebugWindow when running in debug (unless a preview or apply-update command). CommandContext.IsDebugMode is extended to include DOTNET_ENVIRONMENT=Development via a new IsDevelopmentEnvironment helper. Program.Main and BuildAvaloniaApp are made public to aid tooling. Added multiple launchSettings profiles for debug and preview commands that set DOTNET_ENVIRONMENT=Development to simplify IDE debugging and UI previewing.

* Simplify splash to fade; add themed about banners

Simplify splash startup visuals by removing the multi-mode/slide behavior and always using a fade animation. Update App to create SplashWindow without a StartupVisualMode parameter and remove related fields, layout configuration, slide animation, and easing helpers from SplashWindow. Clean up unused using. Replace the single about_banner asset with theme-aware variants (about_banner_dark.png and about_banner_light.png), delete the old about_banner.png, and update AboutSettingsPage to use a DynamicResource ImageBrush (AboutBannerBrush) that selects the appropriate banner per theme.

* Use AppJsonContext for startup state serialization

Switch serialization to the source-generated System.Text.Json context: add JsonSerializable(typeof(StartupAttemptRecord)) to AppJsonContext and replace the previous JsonSerializerOptions-based Serialize/Deserialize calls with AppJsonContext.Default.StartupAttemptRecord. Also remove the now-unused SerializerOptions field. Additionally, update .gitignore to exclude /test-aot-publish.

* Add OOBE redesign, theme & data location support

Introduce a redesigned OOBE flow and data-location/theme support across the launcher. Adds a new ThemeService for applying light/dark and accent colors; integrates FluentIcons.Avalonia package for icons. Overhauls OobeWindow (UX animations, typing effect, multi-step theme and data-location pages, Monet options, and final welcome step) and its code-behind to handle step navigation, accent selection, and data-location resolution. Adds DataLocation UI and handlers (DataLocationPromptWindow changes, DataLocation resolver usage) and wires a DevDebug UI for toggling/opening the data-location page. UpdateEngineService now resolves the launcher root via DataLocationResolver. Misc: update various view models, localization entries and remove TrimmerRoots.xml.

* Refactor data location paths and add background service

Refactor DataLocationResolver to centralize data path resolution (ResolveLauncherDataPath, ResolveDesktopDataPath, ResolveConfigPath, ResolveLauncherLogsPath, ResolveLauncherStatePath) and replace usages of the previous ".launcher" layout with a "Launcher" folder. Update API: LoadConfig/SaveConfig reorganized and ApplyLocationChoice now accepts an optional custom path and migration flag; migration logic updated accordingly. Update dependent services and views (Logger, DeploymentLocator, UpdateEngineService, OobeStateService, StartupAttemptRegistry, LauncherDebugSettingsStore, OobeWindow) to use the new resolver APIs and paths. Add LauncherBackgroundService to load/validate/cache a custom splash background image and wire it into SplashWindow (AXAML/Axaml.cs) with UI placeholders and overlay. Misc: minor cleanup of Oobe/Splash XAML and related code adjustments and logging improvements.
This commit is contained in:
lincube
2026-04-25 18:41:26 +08:00
committed by GitHub
parent 0085c66514
commit 0b603384b4
55 changed files with 3512 additions and 429 deletions

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -150,6 +150,37 @@ public partial class App : Application
_settingsFacade.Settings.Changed += OnSettingsChanged;
_appearanceThemeService.Changed += OnAppearanceThemeChanged;
// 监听系统主题变化
PropertyChanged += OnAppPropertyChanged;
}
private void OnAppPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == ActualThemeVariantProperty)
{
// 系统主题变化时,检查是否需要更新
var themeMode = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ThemeMode;
if (string.Equals(themeMode, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
var newThemeVariant = (ThemeVariant?)e.NewValue;
var isDark = newThemeVariant == ThemeVariant.Dark;
// 同步到设置
var currentThemeState = _settingsFacade.Theme.Get();
if (currentThemeState.IsNightMode != isDark)
{
_settingsFacade.Theme.Save(currentThemeState with { IsNightMode = isDark });
}
// 应用主题
Dispatcher.UIThread.Post(() =>
{
ApplyThemeFromSettings();
RefreshTrayIconContent();
}, DispatcherPriority.Background);
}
}
}
public override void Initialize()
@@ -762,9 +793,30 @@ public partial class App : Application
private void ApplyThemeFromSettings()
{
var snapshot = _appearanceThemeService.GetCurrent();
RequestedThemeVariant = snapshot.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
var themeMode = _settingsFacade.Settings.LoadSnapshot<AppSettingsSnapshot>(SettingsScope.App).ThemeMode;
// 处理跟随系统主题模式
if (string.Equals(themeMode, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
// 使用 Avalonia 的系统主题检测
var systemTheme = ActualThemeVariant;
RequestedThemeVariant = systemTheme;
// 同步 IsNightMode 到设置
var isSystemDark = systemTheme == ThemeVariant.Dark;
var currentThemeState = _settingsFacade.Theme.Get();
if (currentThemeState.IsNightMode != isSystemDark)
{
_settingsFacade.Theme.Save(currentThemeState with { IsNightMode = isSystemDark });
}
}
else
{
RequestedThemeVariant = snapshot.IsNightMode
? ThemeVariant.Dark
: ThemeVariant.Light;
}
ApplyAdaptiveThemeResources();
}
@@ -1054,6 +1106,7 @@ public partial class App : Application
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.CornerRadiusStyle), StringComparer.OrdinalIgnoreCase) ||
(string.Equals(liveAppearance.ThemeColorMode, ThemeAppearanceValues.ColorModeSeedMonet, StringComparison.OrdinalIgnoreCase) &&

Binary file not shown.

Before

Width:  |  Height:  |  Size: 999 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 682 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 MiB

View File

@@ -349,6 +349,11 @@
"settings.appearance.title": "Appearance",
"settings.appearance.description": "Adjust theme source, system material, and window chrome.",
"settings.appearance.theme_header": "Theme",
"settings.appearance.theme_mode_label": "Theme mode",
"settings.appearance.theme_mode_desc": "Choose light, dark, or follow system theme.",
"settings.appearance.theme_mode.light": "Light",
"settings.appearance.theme_mode.dark": "Dark",
"settings.appearance.theme_mode.follow_system": "Follow system",
"settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome",
"settings.color.theme_color_label": "Theme accent color",

View File

@@ -292,6 +292,11 @@
"settings.appearance.title": "外観",
"settings.appearance.description": "テーマソース、システムマテリアル、ウィンドウクロームを調整します。",
"settings.appearance.theme_header": "テーマ",
"settings.appearance.theme_mode_label": "テーマモード",
"settings.appearance.theme_mode_desc": "ライト、ダーク、またはシステムに従うを選択してください。",
"settings.appearance.theme_mode.light": "ライト",
"settings.appearance.theme_mode.dark": "ダーク",
"settings.appearance.theme_mode.follow_system": "システムに従う",
"settings.color.enable_night_mode_toggle": "夜モードを有効にする",
"settings.color.use_system_chrome_toggle": "システムのウィンドウクロームを使用",
"settings.color.theme_color_label": "テーマのアクセントカラー",

View File

@@ -338,6 +338,11 @@
"settings.appearance.title": "외관",
"settings.appearance.description": "테마 소스, 시스템 소재 및 창 외관을 조정합니다.",
"settings.appearance.theme_header": "테마",
"settings.appearance.theme_mode_label": "테마 모드",
"settings.appearance.theme_mode_desc": "라이트, 다크 또는 시스템 설정 따르기를 선택하세요.",
"settings.appearance.theme_mode.light": "라이트",
"settings.appearance.theme_mode.dark": "다크",
"settings.appearance.theme_mode.follow_system": "시스템 설정 따르기",
"settings.color.enable_night_mode_toggle": "야간 모드 활성화",
"settings.color.use_system_chrome_toggle": "시스템 창 제목 표시줄 사용",
"settings.color.theme_color_label": "테마 강조 색상",

View File

@@ -344,6 +344,11 @@
"settings.appearance.title": "外观",
"settings.appearance.description": "调整主题来源、系统材质与窗口外观。",
"settings.appearance.theme_header": "主题",
"settings.appearance.theme_mode_label": "主题模式",
"settings.appearance.theme_mode_desc": "选择日间、夜间或跟随系统主题。",
"settings.appearance.theme_mode.light": "日间",
"settings.appearance.theme_mode.dark": "夜间",
"settings.appearance.theme_mode.follow_system": "跟随系统",
"settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
"settings.color.theme_color_label": "主题强调色",

View File

@@ -27,6 +27,8 @@ public sealed class AppSettingsSnapshot
public string? SelectedWallpaperSeed { get; set; }
public string ThemeMode { get; set; } = "light";
public string? WallpaperPath { get; set; }
public string WallpaperType { get; set; } = "Image";

View File

@@ -22,6 +22,7 @@ public sealed class Program
public static void Main(string[] args)
{
AppLogger.Initialize();
AppDataPathProvider.Initialize(args);
DevPluginOptions.Parse(args);
RegisterGlobalExceptionLogging();
var restartParentProcessId = LauncherRuntimeMetadata.GetRestartParentProcessId(args);

View File

@@ -0,0 +1,66 @@
namespace LanMountainDesktop.Services;
public static class AppDataPathProvider
{
private static string? _overriddenDataRoot;
public static void Initialize(string[] args)
{
var dataRoot = ResolveDataRootFromArgs(args);
if (!string.IsNullOrWhiteSpace(dataRoot))
{
_overriddenDataRoot = Path.GetFullPath(dataRoot);
AppLogger.Info("AppDataPath", $"Data root overridden by launcher: '{_overriddenDataRoot}'.");
}
else
{
var envDataRoot = Environment.GetEnvironmentVariable("LMD_DATA_ROOT");
if (!string.IsNullOrWhiteSpace(envDataRoot))
{
_overriddenDataRoot = Path.GetFullPath(envDataRoot);
AppLogger.Info("AppDataPath", $"Data root overridden by environment variable: '{_overriddenDataRoot}'.");
}
}
}
public static string GetDataRoot()
{
if (!string.IsNullOrWhiteSpace(_overriddenDataRoot))
{
return _overriddenDataRoot;
}
return Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
}
public static string GetSettingsDirectory()
{
return GetDataRoot();
}
public static string GetPluginMarketDirectory()
{
return Path.Combine(GetDataRoot(), "PluginMarket");
}
public static string GetWallpapersDirectory()
{
return Path.Combine(GetDataRoot(), "Wallpapers");
}
private static string? ResolveDataRootFromArgs(string[] args)
{
const string prefix = "--data-root=";
foreach (var arg in args)
{
if (arg.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
return arg[prefix.Length..];
}
}
return null;
}
}

View File

@@ -24,8 +24,7 @@ public sealed class AppDatabaseService
public AppDatabaseService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var dataDirectory = Path.Combine(appData, "LanMountainDesktop");
var dataDirectory = AppDataPathProvider.GetDataRoot();
_databasePath = Path.Combine(dataDirectory, "app.db");
}

View File

@@ -27,8 +27,7 @@ public sealed class AppSettingsService
public AppSettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsDirectory = Path.Combine(appData, "LanMountainDesktop");
var settingsDirectory = AppDataPathProvider.GetSettingsDirectory();
_settingsPath = Path.Combine(settingsDirectory, "settings.json");
}

View File

@@ -30,8 +30,7 @@ public sealed class LauncherSettingsService
public LauncherSettingsService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var settingsDirectory = Path.Combine(appData, "LanMountainDesktop");
var settingsDirectory = AppDataPathProvider.GetSettingsDirectory();
_settingsPath = Path.Combine(settingsDirectory, "launcher-settings.json");
_legacyAppSettingsPath = Path.Combine(settingsDirectory, "settings.json");
}

View File

@@ -76,9 +76,7 @@ internal sealed class SqliteComponentDomainStorage :
public SqliteComponentDomainStorage(string? settingsRoot = null)
{
_settingsRoot = string.IsNullOrWhiteSpace(settingsRoot)
? Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop")
? AppDataPathProvider.GetDataRoot()
: settingsRoot.Trim();
_dbPath = Path.Combine(_settingsRoot, "component-state.db");
_layoutJsonPath = Path.Combine(_settingsRoot, "desktop-layout-settings.json");

View File

@@ -33,7 +33,8 @@ public sealed record ThemeAppearanceSettingsState(
string CornerRadiusStyle = GlobalAppearanceSettings.DefaultCornerRadiusStyle,
string ThemeColorMode = ThemeAppearanceValues.ColorModeDefaultNeutral,
string SystemMaterialMode = ThemeAppearanceValues.MaterialNone,
string? SelectedWallpaperSeed = null);
string? SelectedWallpaperSeed = null,
string ThemeMode = ThemeAppearanceValues.ThemeModeLight);
public sealed record StatusBarSettingsState(
IReadOnlyList<string> TopStatusComponentIds,
IReadOnlyList<string> PinnedTaskbarActions,

View File

@@ -167,10 +167,7 @@ internal sealed class WallpaperMediaService : IWallpaperMediaService
public WallpaperMediaService()
{
var appDataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
_wallpapersDirectory = Path.Combine(appDataRoot, "Wallpapers");
_wallpapersDirectory = AppDataPathProvider.GetWallpapersDirectory();
}
public WallpaperMediaType DetectMediaType(string? path)
@@ -269,7 +266,21 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
cornerRadiusStyle,
ThemeAppearanceValues.NormalizeThemeColorMode(snapshot.ThemeColorMode, snapshot.ThemeColor),
ThemeAppearanceValues.NormalizeSystemMaterialMode(snapshot.SystemMaterialMode),
snapshot.SelectedWallpaperSeed);
snapshot.SelectedWallpaperSeed,
NormalizeThemeMode(snapshot.ThemeMode));
}
private static string NormalizeThemeMode(string? value)
{
if (string.Equals(value, ThemeAppearanceValues.ThemeModeDark, StringComparison.OrdinalIgnoreCase))
{
return ThemeAppearanceValues.ThemeModeDark;
}
if (string.Equals(value, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
return ThemeAppearanceValues.ThemeModeFollowSystem;
}
return ThemeAppearanceValues.ThemeModeLight;
}
public void Save(ThemeAppearanceSettingsState state)
@@ -326,6 +337,13 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
changedKeys.Add(nameof(AppSettingsSnapshot.SelectedWallpaperSeed));
}
var normalizedThemeMode = NormalizeThemeMode(state.ThemeMode);
if (!string.Equals(snapshot.ThemeMode, normalizedThemeMode, StringComparison.OrdinalIgnoreCase))
{
snapshot.ThemeMode = normalizedThemeMode;
changedKeys.Add(nameof(AppSettingsSnapshot.ThemeMode));
}
if (changedKeys.Count == 0)
{
return;
@@ -1026,10 +1044,7 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
{
_pluginRuntimeService = pluginRuntimeService;
var dataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"PluginMarket");
var dataRoot = AppDataPathProvider.GetPluginMarketDirectory();
var cacheService = new AirAppMarketCacheService(dataRoot);
_indexService = new AirAppMarketIndexService(cacheService);
if (_pluginRuntimeService is not null)
@@ -1049,10 +1064,7 @@ internal sealed class PluginCatalogSettingsService : IPluginCatalogSettingsServi
return;
}
var dataRoot = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop",
"PluginMarket");
var dataRoot = AppDataPathProvider.GetPluginMarketDirectory();
_installService = new AirAppMarketInstallService(_pluginRuntimeService, dataRoot);
}

View File

@@ -26,9 +26,7 @@ internal sealed class SettingsService : ISettingsService
public SettingsService()
{
var root = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
"LanMountainDesktop");
var root = AppDataPathProvider.GetDataRoot();
_pluginSettingsPath = Path.Combine(root, "plugin-settings.json");
}

View File

@@ -13,6 +13,10 @@ public static class ThemeAppearanceValues
public const string ColorSchemeFollowSystem = "follow_system";
public const string ColorSchemeNative = "native";
public const string ThemeModeLight = "light";
public const string ThemeModeDark = "dark";
public const string ThemeModeFollowSystem = "follow_system";
public const string MaterialNone = "none";
public const string MaterialMica = "mica";
public const string MaterialAcrylic = "acrylic";

View File

@@ -1,35 +0,0 @@
<linker>
<!-- Avalonia and UI framework assemblies that should not be trimmed -->
<assembly fullname="Avalonia" preserve="all" />
<assembly fullname="Avalonia.Controls" preserve="all" />
<assembly fullname="Avalonia.Core" preserve="all" />
<assembly fullname="Avalonia.Dialogs" preserve="all" />
<assembly fullname="Avalonia.Desktop" preserve="all" />
<assembly fullname="Avalonia.Themes.Fluent" preserve="all" />
<assembly fullname="Avalonia.Fonts.Inter" preserve="all" />
<!-- FluentUI packages -->
<assembly fullname="FluentAvaloniaUI" preserve="all" />
<assembly fullname="FluentIcons.Avalonia" preserve="all" />
<assembly fullname="FluentIcons.Avalonia.Fluent" preserve="all" />
<!-- Media and rendering -->
<assembly fullname="LibVLCSharp" preserve="all" />
<assembly fullname="LibVLCSharp.Avalonia" preserve="all" />
<assembly fullname="WebView.Avalonia" preserve="all" />
<assembly fullname="WebView.Avalonia.Desktop" preserve="all" />
<!-- MVVM and utilities -->
<assembly fullname="CommunityToolkit.Mvvm" preserve="all" />
<assembly fullname="YamlDotNet" preserve="all" />
<assembly fullname="DotNetCampus.AvaloniaInkCanvas" preserve="all" />
<assembly fullname="PortAudioSharp2" preserve="all" />
<!-- System assemblies with reflection usage -->
<assembly fullname="System.Drawing.Common" preserve="all" />
<assembly fullname="System.Runtime.WindowsRuntime" preserve="all" />
<assembly fullname="System.ComponentModel.TypeConverter" preserve="all" />
<assembly fullname="System.Reflection" preserve="all" />
<assembly fullname="System.Reflection.Emit" preserve="all" />
<assembly fullname="System.Reflection.Emit.Lightweight" preserve="all" />
</linker>

View File

@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using LanMountainDesktop.ComponentSystem;
@@ -576,10 +579,36 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
RefreshLocalizedText();
ThemeColorModes = CreateThemeColorModes();
ThemeModeOptions = CreateThemeModeOptions();
_isInitializing = true;
Load();
_isInitializing = false;
}
partial void OnSelectedThemeModeChanged(SelectionOption value)
{
if (_isInitializing || value is null)
{
return;
}
// 根据选择的主题模式更新夜间模式状态
var newIsNightMode = value.Value switch
{
ThemeAppearanceValues.ThemeModeDark => true,
ThemeAppearanceValues.ThemeModeLight => false,
ThemeAppearanceValues.ThemeModeFollowSystem => Application.Current?.ActualThemeVariant == ThemeVariant.Dark,
_ => IsNightMode
};
if (IsNightMode != newIsNightMode)
{
IsNightMode = newIsNightMode;
}
PersistCurrentState(restartRequired: false);
}
public event Action<string>? RestartRequested;
@@ -595,6 +624,27 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
[ObservableProperty]
private string _themeColor = string.Empty;
[ObservableProperty]
private IReadOnlyList<SelectionOption> _themeModeOptions = [];
[ObservableProperty]
private SelectionOption _selectedThemeMode = new(ThemeAppearanceValues.ThemeModeLight, "Light");
[ObservableProperty]
private string _themeModeLabel = string.Empty;
[ObservableProperty]
private string _themeModeDescription = string.Empty;
[ObservableProperty]
private string _themeModeLightText = string.Empty;
[ObservableProperty]
private string _themeModeDarkText = string.Empty;
[ObservableProperty]
private string _themeModeFollowSystemText = string.Empty;
[ObservableProperty]
private Color _customSeedPickerValue = DefaultSeedColor;
@@ -797,16 +847,6 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
UpdatePreview(theme);
}
partial void OnIsNightModeChanged(bool value)
{
if (_isInitializing)
{
return;
}
PersistCurrentState(restartRequired: false);
}
partial void OnUseSystemChromeChanged(bool value)
{
if (_isInitializing)
@@ -887,7 +927,11 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
PageTitle = L("settings.appearance.title", "Appearance");
PageDescription = L("settings.appearance.description", "Adjust theme source, material background, and window chrome.");
ThemeHeader = L("settings.appearance.theme_header", "Theme");
NightModeLabel = L("settings.color.enable_night_mode_toggle", "Enable night mode");
ThemeModeLabel = L("settings.appearance.theme_mode_label", "Theme mode");
ThemeModeDescription = L("settings.appearance.theme_mode_desc", "Choose light, dark, or follow system preference.");
ThemeModeLightText = L("settings.appearance.theme_mode.light", "Light");
ThemeModeDarkText = L("settings.appearance.theme_mode.dark", "Dark");
ThemeModeFollowSystemText = L("settings.appearance.theme_mode.follow_system", "Follow system");
UseSystemChromeLabel = L("settings.color.use_system_chrome_toggle", "Use system window chrome");
ThemeColorLabel = L("settings.color.theme_color_label", "Theme Accent Color");
ThemeColorModeLabel = L("settings.appearance.theme_color_mode_label", "Theme color source");
@@ -957,6 +1001,26 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
SelectedSystemMaterialMode = SystemMaterialModes.FirstOrDefault(option =>
string.Equals(option.Value, savedSystemMaterialMode, StringComparison.OrdinalIgnoreCase))
?? SystemMaterialModes[0];
// 应用主题模式设置
var savedThemeMode = NormalizeThemeMode(theme.ThemeMode);
SelectedThemeMode = ThemeModeOptions.FirstOrDefault(option =>
string.Equals(option.Value, savedThemeMode, StringComparison.OrdinalIgnoreCase))
?? ThemeModeOptions.FirstOrDefault(o => o.Value == ThemeAppearanceValues.ThemeModeLight)
?? new SelectionOption(ThemeAppearanceValues.ThemeModeLight, ThemeModeLightText);
}
private static string NormalizeThemeMode(string? value)
{
if (string.Equals(value, ThemeAppearanceValues.ThemeModeDark, StringComparison.OrdinalIgnoreCase))
{
return ThemeAppearanceValues.ThemeModeDark;
}
if (string.Equals(value, ThemeAppearanceValues.ThemeModeFollowSystem, StringComparison.OrdinalIgnoreCase))
{
return ThemeAppearanceValues.ThemeModeFollowSystem;
}
return ThemeAppearanceValues.ThemeModeLight;
}
private void PersistCurrentState(bool restartRequired)
@@ -984,6 +1048,16 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
}
}
private IReadOnlyList<SelectionOption> CreateThemeModeOptions()
{
return
[
new SelectionOption(ThemeAppearanceValues.ThemeModeLight, ThemeModeLightText),
new SelectionOption(ThemeAppearanceValues.ThemeModeDark, ThemeModeDarkText),
new SelectionOption(ThemeAppearanceValues.ThemeModeFollowSystem, ThemeModeFollowSystemText)
];
}
private ThemeAppearanceSettingsState BuildPendingState(bool usePickerSeed)
{
var themeColorMode = ThemeAppearanceValues.NormalizeThemeColorMode(SelectedThemeColorMode?.Value, ThemeColor);
@@ -998,7 +1072,8 @@ public sealed partial class AppearanceSettingsPageViewModel : ViewModelBase
GlobalAppearanceSettings.NormalizeCornerRadiusStyle(CornerRadiusStyle),
themeColorMode,
ThemeAppearanceValues.NormalizeSystemMaterialMode(SelectedSystemMaterialMode?.Value),
_selectedWallpaperSeed);
_selectedWallpaperSeed,
SelectedThemeMode?.Value ?? ThemeAppearanceValues.ThemeModeLight);
}
private void UpdatePreview(ThemeAppearanceSettingsState pendingState)

View File

@@ -6,6 +6,19 @@
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
x:DataType="vm:AboutSettingsPageViewModel">
<UserControl.Resources>
<ResourceDictionary>
<ResourceDictionary.ThemeDictionaries>
<ResourceDictionary x:Key="Default">
<ImageBrush x:Key="AboutBannerBrush" Source="/Assets/about_banner_light.png" Stretch="Uniform" AlignmentX="Center" AlignmentY="Center" />
</ResourceDictionary>
<ResourceDictionary x:Key="Dark">
<ImageBrush x:Key="AboutBannerBrush" Source="/Assets/about_banner_dark.png" Stretch="Uniform" AlignmentX="Center" AlignmentY="Center" />
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>
</UserControl.Resources>
<UserControl.Styles>
<Style Selector="StackPanel.about-page-container">
<Setter Property="HorizontalAlignment" Value="Stretch" />
@@ -38,10 +51,7 @@
Classes="about-hero-card"
Height="240"
PointerPressed="OnAboutHeroCardPointerPressed">
<Image Source="/Assets/about_banner.png"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
<Panel Background="{DynamicResource AboutBannerBrush}" />
</Border>
<TextBlock Classes="settings-subsection-title"

View File

@@ -13,12 +13,21 @@
Text="{Binding ThemeHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding NightModeLabel}">
<ui:SettingsExpander Header="{Binding ThemeModeLabel}"
Description="{Binding ThemeModeDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="WeatherMoon" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding IsNightMode}" />
<ComboBox Width="200"
ItemsSource="{Binding ThemeModeOptions}"
SelectedItem="{Binding SelectedThemeMode}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>