diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b3c34d7..6f7c1c5 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -25,6 +25,8 @@ jobs:
runs-on: ubuntu-latest
outputs:
version: ${{ steps.version.outputs.version }}
+ assembly_version: ${{ steps.version.outputs.assembly_version }}
+ informational_version: ${{ steps.version.outputs.informational_version }}
tag: ${{ steps.version.outputs.tag }}
checkout_ref: ${{ steps.version.outputs.checkout_ref }}
@@ -47,8 +49,15 @@ jobs:
CHECKOUT_REF="${GITHUB_SHA}"
fi
VERSION="${TAG#v}"
+ IFS='.' read -r -a VERSION_PARTS <<< "${VERSION}"
+ while [ "${#VERSION_PARTS[@]}" -lt 4 ]; do
+ VERSION_PARTS+=("0")
+ done
+ ASSEMBLY_VERSION="${VERSION_PARTS[0]}.${VERSION_PARTS[1]}.${VERSION_PARTS[2]}.${VERSION_PARTS[3]}"
echo "tag=${TAG}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $GITHUB_OUTPUT
+ echo "assembly_version=${ASSEMBLY_VERSION}" >> $GITHUB_OUTPUT
+ echo "informational_version=${VERSION}" >> $GITHUB_OUTPUT
echo "checkout_ref=${CHECKOUT_REF}" >> $GITHUB_OUTPUT
build-windows:
@@ -73,26 +82,16 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- - name: Update version in .csproj
- run: |
- $VERSION = "${{ needs.prepare.outputs.version }}"
- $csprojFiles = @(
- "LanMountainDesktop/LanMountainDesktop.csproj"
- )
-
- foreach ($csprojPath in $csprojFiles) {
- Write-Host "Updating version in $csprojPath to $VERSION"
- $content = Get-Content $csprojPath -Raw
- $content = $content -replace '.*?', "$VERSION"
- Set-Content $csprojPath $content
- }
- shell: pwsh
-
- name: Restore
run: dotnet restore ${{ env.Solution_Name }}
- name: Build
- run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ run: >
+ dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ -p:Version=${{ needs.prepare.outputs.version }}
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
- name: Publish
run: |
@@ -106,7 +105,11 @@ jobs:
-p:DebugType=none `
-p:DebugSymbols=false `
-p:PublishTrimmed=false `
- -p:PublishReadyToRun=false
+ -p:PublishReadyToRun=false `
+ -p:Version=${{ needs.prepare.outputs.version }} `
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} `
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} `
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
shell: pwsh
- name: Install Inno Setup
@@ -242,17 +245,16 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- - name: Update version in .csproj
- run: |
- VERSION="${{ needs.prepare.outputs.version }}"
- echo "Updating version in LanMountainDesktop.csproj to $VERSION"
- sed -i "s/.*<\/Version>/$VERSION<\/Version>/" LanMountainDesktop/LanMountainDesktop.csproj
-
- name: Restore
run: dotnet restore ${{ env.Solution_Name }}
- name: Build
- run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ run: >
+ dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ -p:Version=${{ needs.prepare.outputs.version }}
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
- name: Publish
run: |
@@ -266,7 +268,11 @@ jobs:
-p:DebugType=none \
-p:DebugSymbols=false \
-p:PublishTrimmed=false \
- -p:PublishReadyToRun=false
+ -p:PublishReadyToRun=false \
+ -p:Version=${{ needs.prepare.outputs.version }} \
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
- name: Package as DEB
run: |
@@ -384,17 +390,16 @@ jobs:
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- - name: Update version in .csproj
- run: |
- VERSION="${{ needs.prepare.outputs.version }}"
- echo "Updating version in LanMountainDesktop.csproj to $VERSION"
- sed -i '' "s/.*<\/Version>/$VERSION<\/Version>/" LanMountainDesktop/LanMountainDesktop.csproj
-
- name: Restore
run: dotnet restore ${{ env.Solution_Name }}
- name: Build
- run: dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ run: >
+ dotnet build ${{ env.Solution_Name }} -c Release --no-restore -v minimal
+ -p:Version=${{ needs.prepare.outputs.version }}
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }}
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
- name: Publish
run: |
@@ -408,7 +413,11 @@ jobs:
-p:DebugType=none \
-p:DebugSymbols=false \
-p:PublishTrimmed=false \
- -p:PublishReadyToRun=false
+ -p:PublishReadyToRun=false \
+ -p:Version=${{ needs.prepare.outputs.version }} \
+ -p:AssemblyVersion=${{ needs.prepare.outputs.assembly_version }} \
+ -p:FileVersion=${{ needs.prepare.outputs.assembly_version }} \
+ -p:InformationalVersion=${{ needs.prepare.outputs.informational_version }}
- name: Package as DMG
run: |
diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index 3a03541..dfeb85c 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -493,7 +493,10 @@ public partial class App : Application
var themeChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
- changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase);
+ changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase);
var languageChanged =
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
@@ -516,16 +519,32 @@ public partial class App : Application
private void ApplyAdaptiveThemeResources(ThemeAppearanceSettingsState themeState)
{
- var accentColor = TryParseThemeColor(themeState.ThemeColor);
+ var wallpaperState = _settingsFacade.Wallpaper.Get();
+ var monetPalette = _settingsFacade.Theme.BuildPalette(
+ themeState.IsNightMode,
+ wallpaperState.WallpaperPath,
+ themeState.ThemeColor);
+ var accentColor = ResolveAccentColor(themeState.ThemeColor, monetPalette);
var context = new ThemeColorContext(
accentColor,
IsLightBackground: !themeState.IsNightMode,
IsLightNavBackground: !themeState.IsNightMode,
- IsNightMode: themeState.IsNightMode);
+ IsNightMode: themeState.IsNightMode,
+ MonetColors: monetPalette.MonetColors);
ThemeColorSystemService.ApplyThemeResources(Resources, context);
GlassEffectService.ApplyGlassResources(Resources, context);
}
+ private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
+ {
+ if (monetPalette.MonetColors is { Count: > 0 })
+ {
+ return monetPalette.MonetColors[0];
+ }
+
+ return TryParseThemeColor(colorText);
+ }
+
private static Color TryParseThemeColor(string? colorText)
{
if (!string.IsNullOrWhiteSpace(colorText))
@@ -589,6 +608,14 @@ public partial class App : Application
_exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
_settingsFacade.Settings.Changed -= OnSettingsChanged;
+ try
+ {
+ HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit();
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("UpdateWorkflow", "Failed to apply pending update during exit cleanup.", ex);
+ }
try
{
diff --git a/LanMountainDesktop/Assets/about_banner.png b/LanMountainDesktop/Assets/about_banner.png
new file mode 100644
index 0000000..db9d81c
Binary files /dev/null and b/LanMountainDesktop/Assets/about_banner.png differ
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index 02caa3d..3bd6c25 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -242,7 +242,7 @@
"settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance",
- "settings.appearance.description": "Adjust theme and status bar presentation.",
+ "settings.appearance.description": "Adjust theme, wallpaper, and window chrome.",
"settings.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome",
@@ -310,10 +310,36 @@
"settings.about.render_mode.current_format": "Current backend: {0}",
"settings.about.render_mode.impl_format": "Runtime implementation: {0}",
"settings.about.render_mode.impl_unavailable": "Runtime implementation details are unavailable.",
- "settings.about.description": "Application details and update preferences.",
+ "settings.about.description": "Application details.",
+ "settings.update.description": "Check releases, choose the update channel and download source, and control how updates are installed.",
+ "settings.update.status_card_title": "Update Status",
+ "settings.update.status_card_description": "Check for updates, review release details, and continue with download or installation when a new version is available.",
+ "settings.update.preferences_header": "Update Preferences",
+ "settings.update.preferences_description": "Choose the release channel, installer download source, installation behavior, and download parallelism.",
+ "settings.update.last_checked_label": "Last Checked",
+ "settings.update.source_label": "Download Source",
+ "settings.update.source_github": "GitHub",
+ "settings.update.source_ghproxy": "gh-proxy",
+ "settings.update.source_github_desc": "Download release assets directly from GitHub.",
+ "settings.update.source_ghproxy_desc": "Use the gh-proxy mirror when downloading GitHub release assets.",
+ "settings.update.mode_label": "Update Mode",
+ "settings.update.mode_manual": "Manual Update",
+ "settings.update.mode_download_then_confirm": "Silent Download",
+ "settings.update.mode_silent_on_exit": "Silent Install",
+ "settings.update.mode_manual_desc": "Only check for updates. You decide when downloads and installation happen.",
+ "settings.update.mode_download_then_confirm_desc": "Download updates in the background and ask for confirmation before installing them.",
+ "settings.update.mode_silent_on_exit_desc": "Download updates in the background and install them the next time you exit the app.",
+ "settings.update.channel_stable_desc": "Stable builds prioritize reliability and are recommended for most users.",
+ "settings.update.channel_preview_desc": "Preview builds may contain newer features but can be less stable.",
+ "settings.update.download_threads_label": "Download Threads",
+ "settings.update.download_threads_desc": "Set the number of parallel download threads for application update packages.",
+ "settings.update.install_now_button": "Install Now",
+ "settings.update.status_downloaded_confirm": "Update downloaded. Review it and choose when to install.",
+ "settings.update.status_downloaded_exit": "Update downloaded. It will be installed when you exit the app.",
"settings.about.app_info_header": "Application Information",
"settings.about.update_header": "Updates",
"settings.about.version_label": "Version",
+ "settings.about.codename_label": "Codename",
"settings.about.render_backend_label": "Render Backend",
"settings.about.render_backend_format": "Render Backend: {0}",
"settings.restart_dialog.title": "Restart required",
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index 94d08a6..e8158f7 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -247,7 +247,7 @@
"settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观",
- "settings.appearance.description": "切换主题与状态栏展示。",
+ "settings.appearance.description": "调整主题、壁纸与窗口外观。",
"settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
@@ -315,10 +315,36 @@
"settings.about.render_mode.current_format": "当前后端:{0}",
"settings.about.render_mode.impl_format": "运行时实现:{0}",
"settings.about.render_mode.impl_unavailable": "当前无法获取运行时实现信息。",
- "settings.about.description": "应用信息与更新偏好。",
+ "settings.about.description": "应用信息。",
+ "settings.update.description": "检查更新、选择发布通道与下载源,并控制更新安装方式。",
+ "settings.update.status_card_title": "更新状态",
+ "settings.update.status_card_description": "检查新版本、查看发布信息,并在有更新时继续下载或安装。",
+ "settings.update.preferences_header": "更新偏好",
+ "settings.update.preferences_description": "选择发布通道、安装包下载源、安装方式以及下载并行线程数。",
+ "settings.update.last_checked_label": "上次检查",
+ "settings.update.source_label": "下载源",
+ "settings.update.source_github": "GitHub",
+ "settings.update.source_ghproxy": "gh-proxy",
+ "settings.update.source_github_desc": "直接从 GitHub 下载发布安装包。",
+ "settings.update.source_ghproxy_desc": "下载 GitHub 发布安装包时使用 gh-proxy 镜像。",
+ "settings.update.mode_label": "更新模式",
+ "settings.update.mode_manual": "手动更新",
+ "settings.update.mode_download_then_confirm": "静默下载",
+ "settings.update.mode_silent_on_exit": "静默安装",
+ "settings.update.mode_manual_desc": "仅检查更新,何时下载和安装都由你决定。",
+ "settings.update.mode_download_then_confirm_desc": "后台下载更新,下载完成后由你确认是否安装。",
+ "settings.update.mode_silent_on_exit_desc": "后台下载更新,并在你下次退出应用时静默安装。",
+ "settings.update.channel_stable_desc": "正式版以稳定性优先,适合大多数用户。",
+ "settings.update.channel_preview_desc": "预览版可能包含更早的新功能,但稳定性可能较低。",
+ "settings.update.download_threads_label": "下载线程数",
+ "settings.update.download_threads_desc": "设置应用更新安装包使用的并行下载线程数。",
+ "settings.update.install_now_button": "立即安装",
+ "settings.update.status_downloaded_confirm": "更新已下载完成,请查看并选择安装时机。",
+ "settings.update.status_downloaded_exit": "更新已下载完成,将在你退出应用时安装。",
"settings.about.app_info_header": "应用信息",
"settings.about.update_header": "更新",
"settings.about.version_label": "版本",
+ "settings.about.codename_label": "版本代号",
"settings.about.render_backend_label": "渲染后端",
"settings.about.render_backend_format": "渲染后端:{0}",
"settings.restart_dialog.title": "需要重启应用",
diff --git a/LanMountainDesktop/Models/AppSettingsSnapshot.cs b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
index 1202fca..3120c07 100644
--- a/LanMountainDesktop/Models/AppSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/AppSettingsSnapshot.cs
@@ -60,7 +60,21 @@ public sealed class AppSettingsSnapshot
public bool IncludePrereleaseUpdates { get; set; }
- public string UpdateChannel { get; set; } = string.Empty;
+ public string UpdateChannel { get; set; } = "stable";
+
+ public string UpdateMode { get; set; } = "download_then_confirm";
+
+ public string UpdateDownloadSource { get; set; } = "github";
+
+ public int UpdateDownloadThreads { get; set; } = 4;
+
+ public string? PendingUpdateInstallerPath { get; set; }
+
+ public string? PendingUpdateVersion { get; set; }
+
+ public long? PendingUpdatePublishedAtUtcMs { get; set; }
+
+ public long? LastUpdateCheckUtcMs { get; set; }
public List TopStatusComponentIds { get; set; } = [];
diff --git a/LanMountainDesktop/Services/ComponentEditorWindowService.cs b/LanMountainDesktop/Services/ComponentEditorWindowService.cs
index bf5ef0e..5749347 100644
--- a/LanMountainDesktop/Services/ComponentEditorWindowService.cs
+++ b/LanMountainDesktop/Services/ComponentEditorWindowService.cs
@@ -124,7 +124,10 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
var themeState = _settingsFacade.Theme.Get();
var wallpaperState = _settingsFacade.Wallpaper.Get();
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperState.WallpaperPath);
- var monetPalette = _settingsFacade.Theme.BuildPalette(themeState.IsNightMode, wallpaperState.WallpaperPath);
+ var monetPalette = _settingsFacade.Theme.BuildPalette(
+ themeState.IsNightMode,
+ wallpaperState.WallpaperPath,
+ themeState.ThemeColor);
var palette = ComponentEditorMaterialThemeAdapter.Build(
themeState,
wallpaperState,
diff --git a/LanMountainDesktop/Services/GitHubReleaseUpdateService.cs b/LanMountainDesktop/Services/GitHubReleaseUpdateService.cs
index 174b7ae..b2a6f0a 100644
--- a/LanMountainDesktop/Services/GitHubReleaseUpdateService.cs
+++ b/LanMountainDesktop/Services/GitHubReleaseUpdateService.cs
@@ -172,6 +172,8 @@ public sealed class GitHubReleaseUpdateService : IDisposable
public async Task DownloadAssetAsync(
GitHubReleaseAsset asset,
string destinationFilePath,
+ string downloadSource,
+ int maxParallelSegments,
IProgress? progress = null,
CancellationToken cancellationToken = default)
{
@@ -193,11 +195,14 @@ public sealed class GitHubReleaseUpdateService : IDisposable
var progressAdapter = progress is null
? null
: new Progress(info => progress.Report(info.Progress));
+ var effectiveSource = ApplyDownloadSource(asset.BrowserDownloadUrl, downloadSource);
var result = await _downloadService.DownloadAsync(
- asset.BrowserDownloadUrl,
+ effectiveSource,
destinationFilePath,
- new DownloadOptions(ExpectedSizeBytes: asset.SizeBytes > 0 ? asset.SizeBytes : null),
+ new DownloadOptions(
+ ExpectedSizeBytes: asset.SizeBytes > 0 ? asset.SizeBytes : null,
+ MaxParallelSegments: UpdateSettingsValues.NormalizeDownloadThreads(maxParallelSegments)),
progressAdapter,
cancellationToken);
@@ -460,4 +465,23 @@ public sealed class GitHubReleaseUpdateService : IDisposable
return value[..maxLength];
}
+
+ private static string ApplyDownloadSource(string browserDownloadUrl, string? downloadSource)
+ {
+ if (!string.Equals(
+ UpdateSettingsValues.NormalizeDownloadSource(downloadSource),
+ UpdateSettingsValues.DownloadSourceGhProxy,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return browserDownloadUrl;
+ }
+
+ var normalizedBase = UpdateSettingsValues.DefaultGhProxyBaseUrl.TrimEnd('/') + "/";
+ if (browserDownloadUrl.StartsWith(normalizedBase, StringComparison.OrdinalIgnoreCase))
+ {
+ return browserDownloadUrl;
+ }
+
+ return normalizedBase + browserDownloadUrl;
+ }
}
diff --git a/LanMountainDesktop/Services/GlassEffectService.cs b/LanMountainDesktop/Services/GlassEffectService.cs
index b8ceac8..50bb293 100644
--- a/LanMountainDesktop/Services/GlassEffectService.cs
+++ b/LanMountainDesktop/Services/GlassEffectService.cs
@@ -1,4 +1,5 @@
-using Avalonia.Controls;
+using System.Linq;
+using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Theme;
@@ -6,63 +7,59 @@ namespace LanMountainDesktop.Services;
public static class GlassEffectService
{
- private const double DayPanelBlurRadius = 40;
- private const double DayStrongBlurRadius = 60;
- private const double DayOverlayBlurRadius = 80;
- private const double NightPanelBlurRadius = 45;
- private const double NightStrongBlurRadius = 65;
- private const double NightOverlayBlurRadius = 85;
-
public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context)
{
- // Mica 材质:不透明,但混合壁纸颜色
- // 提取壁纸颜色的透明度(0-1),用于控制 Mica 效果强度
- var wallpaperTintOpacity = 0.15; // 壁纸颜色混合比例
-
- var neutralBase = context.IsNightMode ? Color.Parse("#FF202020") : Color.Parse("#FFF3F3F3");
- var neutralElevated = context.IsNightMode ? Color.Parse("#FF2C2C2C") : Color.Parse("#FFFAFAFA");
-
- // Mica 效果:将壁纸颜色混合到中性基色中
- var micaBackground = ColorMath.Blend(neutralBase, context.AccentColor, wallpaperTintOpacity);
- var micaElevated = ColorMath.Blend(neutralElevated, context.AccentColor, wallpaperTintOpacity * 0.8);
-
- // 按钮颜色
- var buttonBackground = context.IsNightMode ?
- Color.FromArgb(0x33, micaBackground.R, micaBackground.G, micaBackground.B) :
- Color.FromArgb(0x4D, micaBackground.R, micaBackground.G, micaBackground.B);
-
+ var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
+ var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
+ var secondary = monetColors.Length > 1
+ ? monetColors[1]
+ : ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12);
+
+ var panelBase = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF101722"), primary, 0.26)
+ : ColorMath.Blend(Color.Parse("#FFF9FBFE"), primary, 0.14);
+ var panelRaised = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF15202C"), secondary, 0.30)
+ : ColorMath.Blend(Color.Parse("#FFFFFFFF"), secondary, 0.18);
+ var overlayBase = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF0E1622"), primary, 0.36)
+ : ColorMath.Blend(Color.Parse("#FFF3F7FD"), primary, 0.20);
+
+ var buttonBackground = Color.FromArgb(
+ context.IsNightMode ? (byte)0x4D : (byte)0x52,
+ panelRaised.R,
+ panelRaised.G,
+ panelRaised.B);
+ var buttonBorder = Color.FromArgb(
+ context.IsNightMode ? (byte)0x36 : (byte)0x26,
+ primary.R,
+ primary.G,
+ primary.B);
+
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
- resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(
- Color.FromArgb(0x1A, neutralElevated.R, neutralElevated.G, neutralElevated.B));
+ resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(
- ColorMath.WithAlpha(buttonBackground, context.IsNightMode ? (byte)0x4D : (byte)0x66));
+ ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.18), context.IsNightMode ? (byte)0x72 : (byte)0x7A));
resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(
- ColorMath.WithAlpha(buttonBackground, context.IsNightMode ? (byte)0x66 : (byte)0x80));
+ ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.30), context.IsNightMode ? (byte)0x8A : (byte)0x8C));
- // 面板颜色 - 使用 Mica 材质
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
- Color.FromArgb(context.IsNightMode ? (byte)0xF0 : (byte)0xF8,
- micaBackground.R, micaBackground.G, micaBackground.B));
+ Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B));
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(
- Color.FromArgb(0x1F, neutralElevated.R, neutralElevated.G, neutralElevated.B));
+ Color.FromArgb(context.IsNightMode ? (byte)0x38 : (byte)0x24, primary.R, primary.G, primary.B));
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
- Color.FromArgb(context.IsNightMode ? (byte)0xF4 : (byte)0xFB,
- micaElevated.R, micaElevated.G, micaElevated.B));
+ Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B));
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(
- Color.FromArgb(0x29, neutralElevated.R, neutralElevated.G, neutralElevated.B));
+ Color.FromArgb(context.IsNightMode ? (byte)0x4A : (byte)0x2C, secondary.R, secondary.G, secondary.B));
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
- Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF2,
- micaBackground.R, micaBackground.G, micaBackground.B));
+ Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B));
- // 模糊半径(Mica 不需要强模糊)
- resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 20.0 : 30.0;
- resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 25.0 : 35.0;
- resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 30.0 : 40.0;
-
- // 不透明度(Mica 材质接近不透明)
- resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.99 : 1.0;
- resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 1.0 : 1.0;
- resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.94 : 0.97;
- resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.01 : 0.008;
+ resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 22.0 : 28.0;
+ resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0;
+ resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0;
+ resources["AdaptiveGlassPanelOpacity"] = 1.0;
+ resources["AdaptiveGlassStrongOpacity"] = 1.0;
+ resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.95 : 0.98;
+ resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.012 : 0.008;
}
}
diff --git a/LanMountainDesktop/Services/MonetColorService.cs b/LanMountainDesktop/Services/MonetColorService.cs
index c8f31f6..33cc00f 100644
--- a/LanMountainDesktop/Services/MonetColorService.cs
+++ b/LanMountainDesktop/Services/MonetColorService.cs
@@ -12,10 +12,10 @@ namespace LanMountainDesktop.Services;
public sealed class MonetColorService
{
- public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode)
+ public MonetPalette BuildPalette(Bitmap? wallpaper, bool nightMode, Color? preferredSeed = null)
{
var recommended = BuildRecommendedPalette(nightMode);
- var seed = TryExtractSeedColor(wallpaper) ?? TryGetSystemMonetSeedColor() ?? Color.Parse("#FF3B82F6");
+ var seed = preferredSeed ?? TryExtractSeedColor(wallpaper) ?? TryGetSystemMonetSeedColor() ?? Color.Parse("#FF3B82F6");
var monet = BuildMonetPalette(seed, nightMode);
return new MonetPalette(recommended, monet);
}
diff --git a/LanMountainDesktop/Services/Settings/SettingsContracts.cs b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
index 650da15..fced06e 100644
--- a/LanMountainDesktop/Services/Settings/SettingsContracts.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsContracts.cs
@@ -37,7 +37,17 @@ public sealed record WeatherSettingsState(
bool NoTlsRequests,
string LocationQuery);
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId);
-public sealed record UpdateSettingsState(bool AutoCheckUpdates, bool IncludePrereleaseUpdates, string UpdateChannel);
+public sealed record UpdateSettingsState(
+ bool AutoCheckUpdates,
+ bool IncludePrereleaseUpdates,
+ string UpdateChannel,
+ string UpdateMode,
+ string UpdateDownloadSource,
+ int UpdateDownloadThreads,
+ string? PendingUpdateInstallerPath,
+ string? PendingUpdateVersion,
+ long? PendingUpdatePublishedAtUtcMs,
+ long? LastUpdateCheckUtcMs);
public sealed record PluginManagementSettingsState(IReadOnlyList DisabledPluginIds);
public sealed record PluginMarketDependencyInfo(
string Id,
@@ -106,7 +116,7 @@ public interface IThemeAppearanceService
{
ThemeAppearanceSettingsState Get();
void Save(ThemeAppearanceSettingsState state);
- MonetPalette BuildPalette(bool nightMode, string? wallpaperPath);
+ MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null);
}
public interface IStatusBarSettingsService
@@ -164,6 +174,8 @@ public interface IUpdateSettingsService
Task DownloadAssetAsync(
GitHubReleaseAsset asset,
string destinationFilePath,
+ string downloadSource,
+ int maxParallelSegments,
IProgress? progress = null,
CancellationToken cancellationToken = default);
}
@@ -197,6 +209,7 @@ public interface IPluginMarketSettingsService
public interface IApplicationInfoService
{
string GetAppVersionText();
+ string GetAppCodenameText();
AppRenderBackendInfo GetRenderBackendInfo();
}
diff --git a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
index 02bef72..50e127d 100644
--- a/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsDomainServices.cs
@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
+using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
+using Avalonia.Media;
using Avalonia.Media.Imaging;
using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk;
@@ -251,9 +253,15 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
]);
}
- public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath)
+ public MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null)
{
Bitmap? bitmap = null;
+ Color? preferredSeed = null;
+
+ if (!string.IsNullOrWhiteSpace(preferredSeedColor) && Color.TryParse(preferredSeedColor, out var parsedSeed))
+ {
+ preferredSeed = parsedSeed;
+ }
try
{
@@ -274,7 +282,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
try
{
- return _monetColorService.BuildPalette(bitmap, nightMode);
+ return _monetColorService.BuildPalette(bitmap, nightMode, preferredSeed);
}
finally
{
@@ -530,18 +538,49 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
public UpdateSettingsState Get()
{
var snapshot = _settingsService.Load();
+ var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
+ snapshot.UpdateChannel,
+ snapshot.IncludePrereleaseUpdates);
return new UpdateSettingsState(
snapshot.AutoCheckUpdates,
- snapshot.IncludePrereleaseUpdates,
- snapshot.UpdateChannel);
+ string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
+ normalizedChannel,
+ UpdateSettingsValues.NormalizeMode(snapshot.UpdateMode),
+ UpdateSettingsValues.NormalizeDownloadSource(snapshot.UpdateDownloadSource),
+ UpdateSettingsValues.NormalizeDownloadThreads(snapshot.UpdateDownloadThreads),
+ snapshot.PendingUpdateInstallerPath,
+ snapshot.PendingUpdateVersion,
+ snapshot.PendingUpdatePublishedAtUtcMs,
+ snapshot.LastUpdateCheckUtcMs);
}
public void Save(UpdateSettingsState state)
{
var snapshot = _settingsService.Load();
+ var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
+ state.UpdateChannel,
+ state.IncludePrereleaseUpdates);
snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
- snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates;
- snapshot.UpdateChannel = state.UpdateChannel;
+ snapshot.IncludePrereleaseUpdates = string.Equals(
+ normalizedChannel,
+ UpdateSettingsValues.ChannelPreview,
+ StringComparison.OrdinalIgnoreCase);
+ snapshot.UpdateChannel = normalizedChannel;
+ snapshot.UpdateMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
+ snapshot.UpdateDownloadSource = UpdateSettingsValues.NormalizeDownloadSource(state.UpdateDownloadSource);
+ snapshot.UpdateDownloadThreads = UpdateSettingsValues.NormalizeDownloadThreads(state.UpdateDownloadThreads);
+ snapshot.PendingUpdateInstallerPath = string.IsNullOrWhiteSpace(state.PendingUpdateInstallerPath)
+ ? null
+ : state.PendingUpdateInstallerPath.Trim();
+ snapshot.PendingUpdateVersion = string.IsNullOrWhiteSpace(state.PendingUpdateVersion)
+ ? null
+ : state.PendingUpdateVersion.Trim();
+ snapshot.PendingUpdatePublishedAtUtcMs = state.PendingUpdatePublishedAtUtcMs is > 0
+ ? state.PendingUpdatePublishedAtUtcMs
+ : null;
+ snapshot.LastUpdateCheckUtcMs = state.LastUpdateCheckUtcMs is > 0
+ ? state.LastUpdateCheckUtcMs
+ : null;
_settingsService.SaveSnapshot(
SettingsScope.App,
snapshot,
@@ -549,7 +588,14 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
[
nameof(AppSettingsSnapshot.AutoCheckUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates),
- nameof(AppSettingsSnapshot.UpdateChannel)
+ nameof(AppSettingsSnapshot.UpdateChannel),
+ nameof(AppSettingsSnapshot.UpdateMode),
+ nameof(AppSettingsSnapshot.UpdateDownloadSource),
+ nameof(AppSettingsSnapshot.UpdateDownloadThreads),
+ nameof(AppSettingsSnapshot.PendingUpdateInstallerPath),
+ nameof(AppSettingsSnapshot.PendingUpdateVersion),
+ nameof(AppSettingsSnapshot.PendingUpdatePublishedAtUtcMs),
+ nameof(AppSettingsSnapshot.LastUpdateCheckUtcMs)
]);
}
@@ -564,10 +610,18 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
public Task DownloadAssetAsync(
GitHubReleaseAsset asset,
string destinationFilePath,
+ string downloadSource,
+ int maxParallelSegments,
IProgress? progress = null,
CancellationToken cancellationToken = default)
{
- return _releaseUpdateService.DownloadAssetAsync(asset, destinationFilePath, progress, cancellationToken);
+ return _releaseUpdateService.DownloadAssetAsync(
+ asset,
+ destinationFilePath,
+ downloadSource,
+ maxParallelSegments,
+ progress,
+ cancellationToken);
}
public void Dispose()
@@ -795,15 +849,50 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
internal sealed class ApplicationInfoService : IApplicationInfoService
{
+ private const string Codename = "Administrate";
+
public string GetAppVersionText()
{
- var version = typeof(App).Assembly.GetName().Version;
- return version is null
- ? "0.0.0"
- : new Version(
- Math.Max(0, version.Major),
- Math.Max(0, version.Minor),
- Math.Max(0, version.Build)).ToString(3);
+ var assembly = typeof(App).Assembly;
+ var informationalVersion = assembly
+ .GetCustomAttribute()?
+ .InformationalVersion;
+ if (!string.IsNullOrWhiteSpace(informationalVersion))
+ {
+ var normalizedInformationalVersion = informationalVersion.Split('+', 2)[0].Trim();
+ if (!string.IsNullOrWhiteSpace(normalizedInformationalVersion))
+ {
+ return normalizedInformationalVersion;
+ }
+ }
+
+ var version = assembly.GetName().Version;
+ if (version is null)
+ {
+ return "0.0.0";
+ }
+
+ if (version.Revision >= 0)
+ {
+ return version.ToString(4);
+ }
+
+ if (version.Build >= 0)
+ {
+ return version.ToString(3);
+ }
+
+ if (version.Minor >= 0)
+ {
+ return version.ToString(2);
+ }
+
+ return version.ToString();
+ }
+
+ public string GetAppCodenameText()
+ {
+ return Codename;
}
public AppRenderBackendInfo GetRenderBackendInfo()
diff --git a/LanMountainDesktop/Services/Settings/SettingsWindowService.cs b/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
index 8185a82..30e3d52 100644
--- a/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
+++ b/LanMountainDesktop/Services/Settings/SettingsWindowService.cs
@@ -281,6 +281,9 @@ internal sealed class SettingsWindowService : ISettingsWindowService
refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.ThemeColor), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperPath), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperType), StringComparer.OrdinalIgnoreCase) ||
+ changedKeys.Contains(nameof(AppSettingsSnapshot.WallpaperColor), StringComparer.OrdinalIgnoreCase) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged)
@@ -307,16 +310,33 @@ internal sealed class SettingsWindowService : ISettingsWindowService
? ThemeVariant.Dark
: ThemeVariant.Light;
- var accentColor = TryParseThemeColor(themeState.ThemeColor);
+ var settingsFacade = HostSettingsFacadeProvider.GetOrCreate();
+ var wallpaperState = settingsFacade.Wallpaper.Get();
+ var monetPalette = settingsFacade.Theme.BuildPalette(
+ themeState.IsNightMode,
+ wallpaperState.WallpaperPath,
+ themeState.ThemeColor);
+ var accentColor = ResolveAccentColor(themeState.ThemeColor, monetPalette);
var context = new ThemeColorContext(
accentColor,
IsLightBackground: !themeState.IsNightMode,
IsLightNavBackground: !themeState.IsNightMode,
- IsNightMode: themeState.IsNightMode);
+ IsNightMode: themeState.IsNightMode,
+ MonetColors: monetPalette.MonetColors);
ThemeColorSystemService.ApplyThemeResources(window.Resources, context);
GlassEffectService.ApplyGlassResources(window.Resources, context);
}
+ private static Color ResolveAccentColor(string? colorText, MonetPalette monetPalette)
+ {
+ if (monetPalette.MonetColors is { Count: > 0 })
+ {
+ return monetPalette.MonetColors[0];
+ }
+
+ return TryParseThemeColor(colorText);
+ }
+
private static Color TryParseThemeColor(string? colorText)
{
if (!string.IsNullOrWhiteSpace(colorText))
diff --git a/LanMountainDesktop/Services/ThemeColorSystemService.cs b/LanMountainDesktop/Services/ThemeColorSystemService.cs
index 41c4727..9262ebf 100644
--- a/LanMountainDesktop/Services/ThemeColorSystemService.cs
+++ b/LanMountainDesktop/Services/ThemeColorSystemService.cs
@@ -1,4 +1,5 @@
-using Avalonia.Controls;
+using System.Linq;
+using Avalonia.Controls;
using Avalonia.Media;
using LanMountainDesktop.Theme;
@@ -67,7 +68,12 @@ public static class ThemeColorSystemService
private static AppThemePalette BuildPalette(ThemeColorContext context)
{
- var accent = context.AccentColor;
+ var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
+ var accent = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
+ var secondarySeed = monetColors.Length > 1
+ ? monetColors[1]
+ : ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.14);
+
var accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22);
var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38);
var accentLight3 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.54);
@@ -76,11 +82,24 @@ public static class ThemeColorSystemService
var accentDark3 = ColorMath.Blend(accent, Color.Parse("#FF020617"), 0.40);
var primary = context.IsNightMode ? accentLight1 : accentDark1;
- var secondary = context.IsNightMode ? accentLight2 : accentDark2;
+ var secondary = context.IsNightMode
+ ? ColorMath.Blend(secondarySeed, Color.Parse("#FFFFFFFF"), 0.16)
+ : ColorMath.Blend(secondarySeed, Color.Parse("#FF111827"), 0.14);
- var surfaceBase = context.IsNightMode ? Color.Parse("#FF0B1220") : Color.Parse("#FFF3F7FB");
- var surfaceRaised = context.IsNightMode ? Color.Parse("#FF1E293B") : Color.Parse("#FFFFFFFF");
- var surfaceOverlay = context.IsNightMode ? Color.Parse("#CC0B1220") : Color.Parse("#CCE2E8F0");
+ var surfaceBase = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF0A1018"), accent, 0.18)
+ : ColorMath.Blend(Color.Parse("#FFF7F9FD"), accent, 0.09);
+ var surfaceRaised = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF121A24"), secondarySeed, 0.24)
+ : ColorMath.Blend(Color.Parse("#FFFCFEFF"), secondarySeed, 0.12);
+ var surfaceOverlayBase = context.IsNightMode
+ ? ColorMath.Blend(Color.Parse("#FF18212D"), accent, 0.28)
+ : ColorMath.Blend(Color.Parse("#FFF1F5FB"), accent, 0.16);
+ var surfaceOverlay = Color.FromArgb(
+ context.IsNightMode ? (byte)0xE8 : (byte)0xF2,
+ surfaceOverlayBase.R,
+ surfaceOverlayBase.G,
+ surfaceOverlayBase.B);
var textPrimaryPreferred = context.IsLightBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC");
var textPrimary = ColorMath.EnsureContrast(textPrimaryPreferred, surfaceRaised, WcagNormalTextContrast);
@@ -96,7 +115,9 @@ public static class ThemeColorSystemService
? ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast)
: ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), surfaceRaised, WcagNormalTextContrast);
- var navSurface = context.IsLightNavBackground ? surfaceRaised : Color.Parse("#FF111827");
+ var navSurface = context.IsLightNavBackground
+ ? ColorMath.Blend(surfaceRaised, accentLight2, 0.08)
+ : ColorMath.Blend(Color.Parse("#FF111827"), accentDark2, 0.24);
var navText = ColorMath.EnsureContrast(
context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"),
navSurface,
@@ -104,16 +125,22 @@ public static class ThemeColorSystemService
var selectedSurfaceForContrast = ColorMath.Blend(accent, navSurface, 0.18);
var navSelectedText = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), selectedSurfaceForContrast, WcagNormalTextContrast);
- var navItemBackground = context.IsLightNavBackground ? Color.Parse("#33FFFFFF") : Color.Parse("#2A0F172A");
+ var navItemBackground = context.IsLightNavBackground
+ ? Color.FromArgb(0x33, surfaceRaised.R, surfaceRaised.G, surfaceRaised.B)
+ : Color.FromArgb(0x38, navSurface.R, navSurface.G, navSurface.B);
var navItemHoverBackground = context.IsLightNavBackground
- ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFFFFFFF"), 0.48), 0x66)
- : ColorMath.WithAlpha(ColorMath.Blend(accentDark1, Color.Parse("#33111827"), 0.32), 0x78);
+ ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, surfaceRaised, 0.30), 0x7A)
+ : ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.26), 0x88);
var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9);
var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast);
var toggleOn = context.IsNightMode ? accent : accentDark1;
- var toggleOff = context.IsNightMode ? Color.Parse("#66475569") : Color.Parse("#66CBD5E1");
- var toggleBorder = context.IsNightMode ? Color.Parse("#80E2E8F0") : Color.Parse("#8094A3B8");
+ var toggleOff = context.IsNightMode
+ ? Color.FromArgb(0x88, accentDark2.R, accentDark2.G, accentDark2.B)
+ : Color.FromArgb(0x88, accentLight2.R, accentLight2.G, accentLight2.B);
+ var toggleBorder = context.IsNightMode
+ ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFF8FAFC"), 0.28), 0x8C)
+ : ColorMath.WithAlpha(ColorMath.Blend(accentDark2, Color.Parse("#FF334155"), 0.26), 0x78);
var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast);
return new AppThemePalette(
diff --git a/LanMountainDesktop/Services/UpdateSettingsValues.cs b/LanMountainDesktop/Services/UpdateSettingsValues.cs
new file mode 100644
index 0000000..bb5e7f4
--- /dev/null
+++ b/LanMountainDesktop/Services/UpdateSettingsValues.cs
@@ -0,0 +1,63 @@
+using System;
+
+namespace LanMountainDesktop.Services;
+
+public static class UpdateSettingsValues
+{
+ public const string ChannelStable = "stable";
+ public const string ChannelPreview = "preview";
+
+ public const string ModeManual = "manual";
+ public const string ModeDownloadThenConfirm = "download_then_confirm";
+ public const string ModeSilentOnExit = "silent_on_exit";
+
+ public const string DownloadSourceGitHub = "github";
+ public const string DownloadSourceGhProxy = "gh-proxy";
+
+ public const int DefaultDownloadThreads = 4;
+ public const int MinDownloadThreads = 1;
+ public const int MaxDownloadThreads = 128;
+ public const string DefaultGhProxyBaseUrl = "https://gh-proxy.com/";
+
+ public static string NormalizeChannel(string? value, bool includePrereleaseFallback = false)
+ {
+ if (string.Equals(value, ChannelPreview, StringComparison.OrdinalIgnoreCase))
+ {
+ return ChannelPreview;
+ }
+
+ if (string.Equals(value, ChannelStable, StringComparison.OrdinalIgnoreCase))
+ {
+ return ChannelStable;
+ }
+
+ return includePrereleaseFallback ? ChannelPreview : ChannelStable;
+ }
+
+ public static string NormalizeMode(string? value)
+ {
+ if (string.Equals(value, ModeManual, StringComparison.OrdinalIgnoreCase))
+ {
+ return ModeManual;
+ }
+
+ if (string.Equals(value, ModeSilentOnExit, StringComparison.OrdinalIgnoreCase))
+ {
+ return ModeSilentOnExit;
+ }
+
+ return ModeDownloadThenConfirm;
+ }
+
+ public static string NormalizeDownloadSource(string? value)
+ {
+ return string.Equals(value, DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase)
+ ? DownloadSourceGhProxy
+ : DownloadSourceGitHub;
+ }
+
+ public static int NormalizeDownloadThreads(int value)
+ {
+ return Math.Clamp(value, MinDownloadThreads, MaxDownloadThreads);
+ }
+}
diff --git a/LanMountainDesktop/Services/UpdateWorkflowService.cs b/LanMountainDesktop/Services/UpdateWorkflowService.cs
new file mode 100644
index 0000000..091bfea
--- /dev/null
+++ b/LanMountainDesktop/Services/UpdateWorkflowService.cs
@@ -0,0 +1,291 @@
+using System;
+using System.ComponentModel;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Threading;
+using System.Threading.Tasks;
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.Services;
+
+public sealed record UpdatePendingInfo(
+ string InstallerPath,
+ string VersionText,
+ DateTimeOffset? PublishedAt);
+
+public sealed record UpdateInstallerLaunchResult(
+ bool Success,
+ bool UserCancelledElevation,
+ string? ErrorMessage);
+
+internal static class HostUpdateWorkflowServiceProvider
+{
+ private static readonly object Gate = new();
+ private static UpdateWorkflowService? _instance;
+
+ public static UpdateWorkflowService GetOrCreate()
+ {
+ lock (Gate)
+ {
+ return _instance ??= new UpdateWorkflowService(HostSettingsFacadeProvider.GetOrCreate());
+ }
+ }
+}
+
+public sealed class UpdateWorkflowService
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly string _updatesDirectory;
+
+ public UpdateWorkflowService(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade ?? throw new ArgumentNullException(nameof(settingsFacade));
+ _updatesDirectory = Path.Combine(
+ Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
+ "LanMountainDesktop",
+ "Updates");
+ }
+
+ public UpdatePendingInfo? GetPendingUpdate()
+ {
+ var state = _settingsFacade.Update.Get();
+ return GetPendingUpdate(state);
+ }
+
+ public async Task CheckForUpdatesAsync(
+ Version currentVersion,
+ CancellationToken cancellationToken = default)
+ {
+ var state = _settingsFacade.Update.Get();
+ var includePrerelease = string.Equals(
+ UpdateSettingsValues.NormalizeChannel(state.UpdateChannel, state.IncludePrereleaseUpdates),
+ UpdateSettingsValues.ChannelPreview,
+ StringComparison.OrdinalIgnoreCase);
+
+ var result = await _settingsFacade.Update.CheckForUpdatesAsync(
+ currentVersion,
+ includePrerelease,
+ cancellationToken);
+
+ SaveState(state with
+ {
+ LastUpdateCheckUtcMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
+ });
+
+ return result;
+ }
+
+ public async Task DownloadReleaseAsync(
+ UpdateCheckResult checkResult,
+ IProgress? progress = null,
+ CancellationToken cancellationToken = default)
+ {
+ ArgumentNullException.ThrowIfNull(checkResult);
+
+ if (!checkResult.Success || !checkResult.IsUpdateAvailable || checkResult.Release is null || checkResult.PreferredAsset is null)
+ {
+ return new UpdateDownloadResult(false, null, "No compatible update asset is available.");
+ }
+
+ var state = _settingsFacade.Update.Get();
+ var existingPending = GetPendingUpdate(state);
+ if (existingPending is not null &&
+ string.Equals(existingPending.VersionText, checkResult.LatestVersionText, StringComparison.OrdinalIgnoreCase) &&
+ File.Exists(existingPending.InstallerPath))
+ {
+ return new UpdateDownloadResult(true, existingPending.InstallerPath, null);
+ }
+
+ Directory.CreateDirectory(_updatesDirectory);
+ var fileName = SanitizeFileName(checkResult.PreferredAsset.Name);
+ var destinationPath = Path.Combine(_updatesDirectory, fileName);
+
+ var result = await _settingsFacade.Update.DownloadAssetAsync(
+ checkResult.PreferredAsset,
+ destinationPath,
+ state.UpdateDownloadSource,
+ state.UpdateDownloadThreads,
+ progress,
+ cancellationToken);
+
+ if (result.Success)
+ {
+ SaveState(state with
+ {
+ PendingUpdateInstallerPath = result.FilePath ?? destinationPath,
+ PendingUpdateVersion = checkResult.LatestVersionText,
+ PendingUpdatePublishedAtUtcMs = checkResult.Release.PublishedAt == DateTimeOffset.MinValue
+ ? null
+ : checkResult.Release.PublishedAt.ToUnixTimeMilliseconds(),
+ LastUpdateCheckUtcMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
+ });
+ }
+
+ return result;
+ }
+
+ public async Task AutoCheckIfEnabledAsync(
+ Version currentVersion,
+ CancellationToken cancellationToken = default)
+ {
+ var state = _settingsFacade.Update.Get();
+ if (!state.AutoCheckUpdates)
+ {
+ return;
+ }
+
+ try
+ {
+ var result = await CheckForUpdatesAsync(currentVersion, cancellationToken);
+ if (!result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
+ {
+ return;
+ }
+
+ var normalizedMode = UpdateSettingsValues.NormalizeMode(state.UpdateMode);
+ if (string.Equals(normalizedMode, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
+ {
+ return;
+ }
+
+ await DownloadReleaseAsync(result, cancellationToken: cancellationToken);
+ }
+ catch (OperationCanceledException)
+ {
+ throw;
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("UpdateWorkflow", "Automatic update check failed.", ex);
+ }
+ }
+
+ public UpdateInstallerLaunchResult LaunchPendingInstallerNow()
+ {
+ return LaunchPendingInstaller(silent: false, exitApplicationAfterLaunch: true);
+ }
+
+ public bool TryApplyPendingUpdateOnExit()
+ {
+ var state = _settingsFacade.Update.Get();
+ if (!string.Equals(
+ UpdateSettingsValues.NormalizeMode(state.UpdateMode),
+ UpdateSettingsValues.ModeSilentOnExit,
+ StringComparison.OrdinalIgnoreCase))
+ {
+ return false;
+ }
+
+ var result = LaunchPendingInstaller(silent: true, exitApplicationAfterLaunch: false);
+ if (!result.Success && !string.IsNullOrWhiteSpace(result.ErrorMessage))
+ {
+ AppLogger.Warn("UpdateWorkflow", $"Silent update on exit failed: {result.ErrorMessage}");
+ }
+
+ return result.Success;
+ }
+
+ public void ClearPendingUpdate()
+ {
+ var state = _settingsFacade.Update.Get();
+ SaveState(state with
+ {
+ PendingUpdateInstallerPath = null,
+ PendingUpdateVersion = null,
+ PendingUpdatePublishedAtUtcMs = null
+ });
+ }
+
+ private UpdateInstallerLaunchResult LaunchPendingInstaller(bool silent, bool exitApplicationAfterLaunch)
+ {
+ var state = _settingsFacade.Update.Get();
+ var pending = GetPendingUpdate(state);
+ if (pending is null)
+ {
+ return new UpdateInstallerLaunchResult(false, false, "No pending installer is available.");
+ }
+
+ try
+ {
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = pending.InstallerPath,
+ WorkingDirectory = Path.GetDirectoryName(pending.InstallerPath) ?? _updatesDirectory,
+ UseShellExecute = true,
+ Verb = OperatingSystem.IsWindows() ? "runas" : string.Empty,
+ Arguments = silent ? "/VERYSILENT /SUPPRESSMSGBOXES /NORESTART" : string.Empty
+ };
+
+ Process.Start(startInfo);
+ ClearPendingUpdate();
+
+ if (exitApplicationAfterLaunch)
+ {
+ App.CurrentHostApplicationLifecycle?.TryExit(new HostApplicationLifecycleRequest(
+ Source: "Update",
+ Reason: silent
+ ? "Silent installer launched."
+ : "Installer launched from update page."));
+ }
+
+ return new UpdateInstallerLaunchResult(true, false, null);
+ }
+ catch (Win32Exception ex) when (ex.NativeErrorCode == 1223)
+ {
+ return new UpdateInstallerLaunchResult(false, true, ex.Message);
+ }
+ catch (Exception ex)
+ {
+ return new UpdateInstallerLaunchResult(false, false, ex.Message);
+ }
+ }
+
+ private UpdatePendingInfo? GetPendingUpdate(UpdateSettingsState state)
+ {
+ var installerPath = state.PendingUpdateInstallerPath?.Trim();
+ if (string.IsNullOrWhiteSpace(installerPath))
+ {
+ return null;
+ }
+
+ if (!File.Exists(installerPath))
+ {
+ ClearPendingUpdate();
+ return null;
+ }
+
+ DateTimeOffset? publishedAt = state.PendingUpdatePublishedAtUtcMs is > 0
+ ? DateTimeOffset.FromUnixTimeMilliseconds(state.PendingUpdatePublishedAtUtcMs.Value)
+ : null;
+
+ return new UpdatePendingInfo(
+ installerPath,
+ string.IsNullOrWhiteSpace(state.PendingUpdateVersion) ? Path.GetFileNameWithoutExtension(installerPath) : state.PendingUpdateVersion,
+ publishedAt);
+ }
+
+ private void SaveState(UpdateSettingsState state)
+ {
+ _settingsFacade.Update.Save(state);
+ }
+
+ private static string SanitizeFileName(string? fileName)
+ {
+ if (string.IsNullOrWhiteSpace(fileName))
+ {
+ return FormattableString.Invariant($"LanMountainDesktop-update-{DateTimeOffset.UtcNow.ToUnixTimeSeconds()}.exe");
+ }
+
+ var invalid = Path.GetInvalidFileNameChars();
+ Span buffer = stackalloc char[fileName.Length];
+ var index = 0;
+ foreach (var ch in fileName)
+ {
+ buffer[index++] = Array.IndexOf(invalid, ch) >= 0 ? '_' : ch;
+ }
+
+ return new string(buffer[..index]);
+ }
+}
diff --git a/LanMountainDesktop/Theme/ThemeColorContext.cs b/LanMountainDesktop/Theme/ThemeColorContext.cs
index 60a4075..49574b6 100644
--- a/LanMountainDesktop/Theme/ThemeColorContext.cs
+++ b/LanMountainDesktop/Theme/ThemeColorContext.cs
@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System.Collections.Generic;
+using Avalonia.Media;
namespace LanMountainDesktop.Theme;
@@ -6,4 +7,5 @@ public sealed record ThemeColorContext(
Color AccentColor,
bool IsLightBackground,
bool IsLightNavBackground,
- bool IsNightMode);
+ bool IsNightMode,
+ IReadOnlyList? MonetColors = null);
diff --git a/LanMountainDesktop/ViewModels/SettingsViewModels.cs b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
index 3f1f39a..7b7949d 100644
--- a/LanMountainDesktop/ViewModels/SettingsViewModels.cs
+++ b/LanMountainDesktop/ViewModels/SettingsViewModels.cs
@@ -1001,96 +1001,293 @@ public sealed partial class AboutSettingsPageViewModel : ViewModelBase
private readonly ISettingsFacadeService _settingsFacade;
private readonly LocalizationService _localizationService = new();
private readonly string _languageCode;
- private bool _isInitializing;
public AboutSettingsPageViewModel(ISettingsFacadeService settingsFacade)
{
_settingsFacade = settingsFacade;
_languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
- UpdateChannels = CreateUpdateChannels();
RefreshLocalizedText();
- var update = _settingsFacade.Update.Get();
- _isInitializing = true;
- AutoCheckUpdates = update.AutoCheckUpdates;
- IncludePrereleaseUpdates = update.IncludePrereleaseUpdates;
- SelectedUpdateChannel = UpdateChannels.FirstOrDefault(option =>
- string.Equals(
- option.Value,
- string.IsNullOrWhiteSpace(update.UpdateChannel) ? "stable" : update.UpdateChannel,
- StringComparison.OrdinalIgnoreCase))
- ?? UpdateChannels[0];
-
- var versionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
+ VersionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
+ CodenameText = _settingsFacade.ApplicationInfo.GetAppCodenameText();
var backendInfo = _settingsFacade.ApplicationInfo.GetRenderBackendInfo();
- var renderBackendText = string.IsNullOrWhiteSpace(backendInfo.ImplementationTypeName)
+ RenderBackendText = string.IsNullOrWhiteSpace(backendInfo.ImplementationTypeName)
? backendInfo.ActualBackend
: $"{backendInfo.ActualBackend} ({backendInfo.ImplementationTypeName})";
- VersionText = string.Format(
- CultureInfo.CurrentCulture,
- L("settings.about.version_format", "Version: {0}"),
- versionText);
- RenderBackendText = string.Format(
- CultureInfo.CurrentCulture,
- L("settings.about.render_backend_format", "Render Backend: {0}"),
- renderBackendText);
- UpdateStatus = L("settings.update.status_idle", "No update check has been performed yet.");
- _isInitializing = false;
}
- public IReadOnlyList UpdateChannels { get; }
-
[ObservableProperty]
private string _versionText = "-";
+ [ObservableProperty]
+ private string _codenameText = "-";
+
[ObservableProperty]
private string _renderBackendText = "-";
- [ObservableProperty]
- private bool _autoCheckUpdates;
-
- [ObservableProperty]
- private bool _includePrereleaseUpdates;
-
- [ObservableProperty]
- private SelectionOption _selectedUpdateChannel = new("stable", "Stable");
-
- [ObservableProperty]
- private string _updateStatus = string.Empty;
-
- [ObservableProperty]
- private bool _isCheckingForUpdates;
-
[ObservableProperty]
private string _pageTitle = string.Empty;
[ObservableProperty]
private string _pageDescription = string.Empty;
- [ObservableProperty]
- private string _autoCheckUpdatesLabel = string.Empty;
-
- [ObservableProperty]
- private string _includePrereleaseUpdatesLabel = string.Empty;
-
- [ObservableProperty]
- private string _updateChannelLabel = string.Empty;
-
- [ObservableProperty]
- private string _checkForUpdatesButtonText = string.Empty;
-
[ObservableProperty]
private string _appInfoHeader = string.Empty;
- [ObservableProperty]
- private string _updateHeader = string.Empty;
-
[ObservableProperty]
private string _versionLabel = string.Empty;
+ [ObservableProperty]
+ private string _codenameLabel = string.Empty;
+
[ObservableProperty]
private string _renderBackendLabel = string.Empty;
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.about.title", "About");
+ PageDescription = L("settings.about.description", "Application details.");
+ AppInfoHeader = L("settings.about.app_info_header", "Application Information");
+ VersionLabel = L("settings.about.version_label", "Version");
+ CodenameLabel = L("settings.about.codename_label", "Codename");
+ RenderBackendLabel = L("settings.about.render_backend_label", "Render Backend");
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
+
+public sealed partial class UpdateSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly UpdateWorkflowService _updateWorkflowService;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private readonly Version _currentVersion;
+ private bool _isInitializing;
+ private UpdateCheckResult? _lastCheckResult;
+
+ public IReadOnlyList UpdateChannelOptions { get; }
+
+ public IReadOnlyList UpdateSourceOptions { get; }
+
+ public IReadOnlyList UpdateModeOptions { get; }
+
+ public IReadOnlyList DownloadThreadOptions { get; }
+
+ public UpdateSettingsPageViewModel(
+ ISettingsFacadeService settingsFacade,
+ UpdateWorkflowService? updateWorkflowService = null)
+ {
+ _settingsFacade = settingsFacade;
+ _updateWorkflowService = updateWorkflowService ?? HostUpdateWorkflowServiceProvider.GetOrCreate();
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+ RefreshLocalizedText();
+ UpdateChannelOptions = CreateUpdateChannelOptions();
+ UpdateSourceOptions = CreateUpdateSourceOptions();
+ UpdateModeOptions = CreateUpdateModeOptions();
+ DownloadThreadOptions = CreateDownloadThreadOptions();
+
+ var versionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
+ _currentVersion = Version.TryParse(versionText, out var parsedVersion)
+ ? parsedVersion
+ : new Version(0, 0, 0);
+
+ CurrentVersionText = versionText;
+ LoadStateFromSettings();
+ }
+
+ [ObservableProperty]
+ private bool _autoCheckUpdates;
+
+ [ObservableProperty]
+ private string _selectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
+
+ [ObservableProperty]
+ private string _selectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
+
+ [ObservableProperty]
+ private string _selectedUpdateModeValue = UpdateSettingsValues.ModeDownloadThenConfirm;
+
+ [ObservableProperty]
+ private string _currentVersionText = "-";
+
+ [ObservableProperty]
+ private string _updateStatus = string.Empty;
+
+ [ObservableProperty]
+ private bool _isCheckingForUpdates;
+
+ [ObservableProperty]
+ private bool _isDownloading;
+
+ [ObservableProperty]
+ private double _downloadProgressValue;
+
+ [ObservableProperty]
+ private bool _isDownloadProgressVisible;
+
+ [ObservableProperty]
+ private string _downloadProgressText = string.Empty;
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _statusCardTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _statusCardDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _preferencesHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _preferencesDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _autoCheckUpdatesLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _updateChannelLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _updateSourceLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _updateModeLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _currentVersionLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _latestVersionLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _publishedAtLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _lastCheckedLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _checkForUpdatesButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _downloadButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _installNowButtonText = string.Empty;
+
+ [ObservableProperty]
+ private string _latestVersionText = string.Empty;
+
+ [ObservableProperty]
+ private string _publishedAtText = string.Empty;
+
+ [ObservableProperty]
+ private string _lastCheckedText = string.Empty;
+
+ [ObservableProperty]
+ private bool _isLatestVersionVisible;
+
+ [ObservableProperty]
+ private bool _isPublishedAtVisible;
+
+ [ObservableProperty]
+ private bool _isLastCheckedVisible;
+
+ [ObservableProperty]
+ private bool _hasPendingInstaller;
+
+ [ObservableProperty]
+ private double _downloadThreadsSliderValue = UpdateSettingsValues.DefaultDownloadThreads;
+
+ [ObservableProperty]
+ private string _selectedUpdateChannelDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _selectedUpdateModeDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _selectedUpdateSourceDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _downloadThreadsLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _downloadThreadsDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _stableChannelText = string.Empty;
+
+ [ObservableProperty]
+ private string _previewChannelText = string.Empty;
+
+ [ObservableProperty]
+ private string _gitHubSourceText = string.Empty;
+
+ [ObservableProperty]
+ private string _ghProxySourceText = string.Empty;
+
+ [ObservableProperty]
+ private string _manualModeText = string.Empty;
+
+ [ObservableProperty]
+ private string _downloadThenConfirmModeText = string.Empty;
+
+ [ObservableProperty]
+ private string _silentOnExitModeText = string.Empty;
+
+ [ObservableProperty]
+ private SelectionOption? _selectedUpdateChannelOption;
+
+ [ObservableProperty]
+ private SelectionOption? _selectedUpdateSourceOption;
+
+ [ObservableProperty]
+ private SelectionOption? _selectedUpdateModeOption;
+
+ [ObservableProperty]
+ private SelectionOption? _selectedDownloadThreadsOption;
+
+ [ObservableProperty]
+ private string _downloadThreadsText = UpdateSettingsValues.DefaultDownloadThreads.ToString(CultureInfo.CurrentCulture);
+
+ public bool IsStableChannelSelected =>
+ string.Equals(SelectedUpdateChannelValue, UpdateSettingsValues.ChannelStable, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsPreviewChannelSelected =>
+ string.Equals(SelectedUpdateChannelValue, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsGitHubSourceSelected =>
+ string.Equals(SelectedUpdateSourceValue, UpdateSettingsValues.DownloadSourceGitHub, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsGhProxySourceSelected =>
+ string.Equals(SelectedUpdateSourceValue, UpdateSettingsValues.DownloadSourceGhProxy, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsManualModeSelected =>
+ string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsDownloadThenConfirmModeSelected =>
+ string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeDownloadThenConfirm, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsSilentOnExitModeSelected =>
+ string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase);
+
+ public bool IsDownloadButtonVisible =>
+ !HasPendingInstaller &&
+ _lastCheckResult is { Success: true, IsUpdateAvailable: true, PreferredAsset: not null };
+
+ public bool IsInstallButtonVisible => HasPendingInstaller;
+
+ public string DownloadThreadsValueText =>
+ UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.CurrentCulture);
+
+ private bool IsBusy => IsCheckingForUpdates || IsDownloading;
+
partial void OnAutoCheckUpdatesChanged(bool value)
{
if (_isInitializing)
@@ -1101,7 +1298,72 @@ public sealed partial class AboutSettingsPageViewModel : ViewModelBase
SaveUpdateSettings();
}
- partial void OnIncludePrereleaseUpdatesChanged(bool value)
+ partial void OnSelectedUpdateChannelOptionChanged(SelectionOption? value)
+ {
+ if (value is not null &&
+ !string.Equals(SelectedUpdateChannelValue, value.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ SelectedUpdateChannelValue = value.Value;
+ }
+ }
+
+ partial void OnSelectedUpdateSourceOptionChanged(SelectionOption? value)
+ {
+ if (value is not null &&
+ !string.Equals(SelectedUpdateSourceValue, value.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ SelectedUpdateSourceValue = value.Value;
+ }
+ }
+
+ partial void OnSelectedUpdateModeOptionChanged(SelectionOption? value)
+ {
+ if (value is not null &&
+ !string.Equals(SelectedUpdateModeValue, value.Value, StringComparison.OrdinalIgnoreCase))
+ {
+ SelectedUpdateModeValue = value.Value;
+ }
+ }
+
+ partial void OnSelectedDownloadThreadsOptionChanged(SelectionOption? value)
+ {
+ if (value is null || !int.TryParse(value.Value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed))
+ {
+ return;
+ }
+
+ ApplyDownloadThreadsValue(parsed, !_isInitializing);
+ }
+
+ partial void OnSelectedUpdateChannelValueChanged(string value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ _lastCheckResult = null;
+ if (!HasPendingInstaller)
+ {
+ LatestVersionText = string.Empty;
+ PublishedAtText = string.Empty;
+ IsLatestVersionVisible = false;
+ IsPublishedAtVisible = false;
+ }
+
+ SaveUpdateSettings();
+ UpdateStatus = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_channel_changed_format", "Update channel switched to {0}. Please check again."),
+ string.Equals(value, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase)
+ ? L("settings.update.channel_preview", "Preview")
+ : L("settings.update.channel_stable", "Stable"));
+ SelectedUpdateChannelDescription = BuildUpdateChannelDescription(value);
+ SyncSelectedOptions();
+ RefreshActionState();
+ }
+
+ partial void OnSelectedUpdateSourceValueChanged(string value)
{
if (_isInitializing)
{
@@ -1109,63 +1371,196 @@ public sealed partial class AboutSettingsPageViewModel : ViewModelBase
}
SaveUpdateSettings();
+ SelectedUpdateSourceDescription = BuildUpdateSourceDescription(value);
+ UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
+ SyncSelectedOptions();
}
- partial void OnSelectedUpdateChannelChanged(SelectionOption value)
+ partial void OnSelectedUpdateModeValueChanged(string value)
{
- if (_isInitializing || value is null)
+ if (_isInitializing)
{
return;
}
SaveUpdateSettings();
+ SelectedUpdateModeDescription = BuildUpdateModeDescription(value);
+ UpdateStatus = HasPendingInstaller
+ ? BuildPendingReadyStatus()
+ : L("settings.update.status_preferences_saved", "Update preferences saved.");
+ SyncSelectedOptions();
+ RefreshActionState();
}
- private void SaveUpdateSettings()
+ partial void OnDownloadThreadsSliderValueChanged(double value)
{
- _settingsFacade.Update.Save(new UpdateSettingsState(
- AutoCheckUpdates,
- IncludePrereleaseUpdates,
- SelectedUpdateChannel.Value));
- UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
- }
+ var normalized = UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(value));
+ if (Math.Abs(value - normalized) > double.Epsilon)
+ {
+ DownloadThreadsSliderValue = normalized;
+ return;
+ }
- [RelayCommand]
- private async Task CheckForUpdatesAsync()
- {
- if (IsCheckingForUpdates)
+ OnPropertyChanged(nameof(DownloadThreadsValueText));
+ if (_isInitializing)
{
return;
}
+ SaveUpdateSettings();
+ UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
+ SyncSelectedOptions();
+ }
+
+ partial void OnDownloadThreadsTextChanged(string value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ if (!TryParseDownloadThreads(value, out var parsed))
+ {
+ return;
+ }
+
+ ApplyDownloadThreadsValue(parsed, true);
+ }
+
+ partial void OnHasPendingInstallerChanged(bool value)
+ {
+ RefreshActionState();
+ if (!value)
+ {
+ UpdateStatus = L("settings.update.status_ready", "Ready to check for updates.");
+ }
+ }
+
+ partial void OnIsCheckingForUpdatesChanged(bool value)
+ {
+ CheckForUpdatesCommand.NotifyCanExecuteChanged();
+ DownloadLatestReleaseCommand.NotifyCanExecuteChanged();
+ InstallPendingUpdateCommand.NotifyCanExecuteChanged();
+ }
+
+ partial void OnIsDownloadingChanged(bool value)
+ {
+ CheckForUpdatesCommand.NotifyCanExecuteChanged();
+ DownloadLatestReleaseCommand.NotifyCanExecuteChanged();
+ InstallPendingUpdateCommand.NotifyCanExecuteChanged();
+ }
+
+ [RelayCommand]
+ private void SelectStableChannel()
+ {
+ SelectedUpdateChannelValue = UpdateSettingsValues.ChannelStable;
+ }
+
+ [RelayCommand]
+ private void SelectPreviewChannel()
+ {
+ SelectedUpdateChannelValue = UpdateSettingsValues.ChannelPreview;
+ }
+
+ [RelayCommand]
+ private void SelectGitHubSource()
+ {
+ SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGitHub;
+ }
+
+ [RelayCommand]
+ private void SelectGhProxySource()
+ {
+ SelectedUpdateSourceValue = UpdateSettingsValues.DownloadSourceGhProxy;
+ }
+
+ [RelayCommand]
+ private void SelectManualMode()
+ {
+ SelectedUpdateModeValue = UpdateSettingsValues.ModeManual;
+ }
+
+ [RelayCommand]
+ private void SelectDownloadThenConfirmMode()
+ {
+ SelectedUpdateModeValue = UpdateSettingsValues.ModeDownloadThenConfirm;
+ }
+
+ [RelayCommand]
+ private void SelectSilentOnExitMode()
+ {
+ SelectedUpdateModeValue = UpdateSettingsValues.ModeSilentOnExit;
+ }
+
+ private void SaveUpdateSettings()
+ {
+ var current = _settingsFacade.Update.Get();
+ _settingsFacade.Update.Save(current with
+ {
+ AutoCheckUpdates = AutoCheckUpdates,
+ IncludePrereleaseUpdates = string.Equals(
+ SelectedUpdateChannelValue,
+ UpdateSettingsValues.ChannelPreview,
+ StringComparison.OrdinalIgnoreCase),
+ UpdateChannel = SelectedUpdateChannelValue,
+ UpdateMode = SelectedUpdateModeValue,
+ UpdateDownloadSource = SelectedUpdateSourceValue,
+ UpdateDownloadThreads = UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue))
+ });
+ }
+
+ private bool CanCheckForUpdates() => !IsBusy;
+
+ [RelayCommand(CanExecute = nameof(CanCheckForUpdates))]
+ private async Task CheckForUpdatesAsync()
+ {
try
{
IsCheckingForUpdates = true;
- var version = Version.TryParse(VersionText, out var currentVersion)
- ? currentVersion
- : new Version(0, 0, 0);
+ IsDownloadProgressVisible = false;
+ DownloadProgressValue = 0;
+ DownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
+ UpdateStatus = L("settings.update.status_checking", "Checking GitHub releases...");
+
+ var result = await _updateWorkflowService.CheckForUpdatesAsync(_currentVersion);
+ _lastCheckResult = result.Success ? result : null;
+ RefreshLastCheckedFromSettings();
- var result = await _settingsFacade.Update.CheckForUpdatesAsync(version, IncludePrereleaseUpdates);
if (!result.Success)
{
UpdateStatus = string.IsNullOrWhiteSpace(result.ErrorMessage)
? L("settings.update.status_check_failed", "Failed to check for updates.")
- : result.ErrorMessage;
+ : string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_check_failed_format", "Update check failed: {0}"),
+ result.ErrorMessage);
return;
}
- UpdateStatus = result.IsUpdateAvailable
- ? string.Format(
- CultureInfo.CurrentCulture,
- L(
- "settings.update.status_available_summary_format",
- "Update available: {0} (current: {1})"),
- result.LatestVersionText,
- result.CurrentVersionText)
- : string.Format(
- CultureInfo.CurrentCulture,
- L("settings.update.status_up_to_date_format", "You are up to date ({0})."),
- result.CurrentVersionText);
+ ApplyCheckResultDisplay(result);
+ if (!result.IsUpdateAvailable)
+ {
+ return;
+ }
+
+ if (result.PreferredAsset is null)
+ {
+ UpdateStatus = L(
+ "settings.update.status_asset_missing",
+ "A new release is available, but no compatible installer was found.");
+ return;
+ }
+
+ if (!string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeManual, StringComparison.OrdinalIgnoreCase))
+ {
+ await DownloadLatestReleaseCoreAsync(result, invokedFromCheck: true);
+ return;
+ }
+
+ UpdateStatus = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_available_format", "New version {0} is available. Click Download & Install."),
+ result.LatestVersionText);
}
finally
{
@@ -1173,27 +1568,345 @@ public sealed partial class AboutSettingsPageViewModel : ViewModelBase
}
}
- private IReadOnlyList CreateUpdateChannels()
+ private bool CanDownloadLatestRelease() => !IsBusy && IsDownloadButtonVisible;
+
+ [RelayCommand(CanExecute = nameof(CanDownloadLatestRelease))]
+ private async Task DownloadLatestReleaseAsync()
{
- return
- [
- new SelectionOption("stable", L("settings.update.channel_stable", "Stable")),
- new SelectionOption("preview", L("settings.update.channel_preview", "Preview"))
- ];
+ await DownloadLatestReleaseCoreAsync(_lastCheckResult, invokedFromCheck: false);
+ }
+
+ private bool CanInstallPendingUpdate() => !IsBusy && HasPendingInstaller;
+
+ [RelayCommand(CanExecute = nameof(CanInstallPendingUpdate))]
+ private void InstallPendingUpdate()
+ {
+ var result = _updateWorkflowService.LaunchPendingInstallerNow();
+ if (result.Success)
+ {
+ UpdateStatus = L(
+ "settings.update.status_installer_started",
+ "Installer started. The app will close for update.");
+ HasPendingInstaller = false;
+ return;
+ }
+
+ UpdateStatus = result.UserCancelledElevation
+ ? L(
+ "settings.update.status_elevation_cancelled",
+ "Administrator permission was not granted. Update was cancelled.")
+ : string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_launch_failed_format", "Failed to start installer: {0}"),
+ result.ErrorMessage ?? L("settings.update.status_installer_missing", "Installer file was not found after download."));
}
private void RefreshLocalizedText()
{
- PageTitle = L("settings.about.title", "About");
- PageDescription = L("settings.about.description", "Application details and update preferences.");
- AppInfoHeader = L("settings.about.app_info_header", "Application Information");
- UpdateHeader = L("settings.about.update_header", "Updates");
- VersionLabel = L("settings.about.version_label", "Version");
- RenderBackendLabel = L("settings.about.render_backend_label", "Render Backend");
+ PageTitle = L("settings.update.title", "Update");
+ PageDescription = L("settings.update.description", "Update checks and release channel preferences.");
+ StatusCardTitle = L("settings.update.status_card_title", "Update Status");
+ StatusCardDescription = L("settings.update.status_card_description", "Check for updates and review the latest release information.");
+ PreferencesHeader = L("settings.update.preferences_header", "Update Preferences");
+ PreferencesDescription = L("settings.update.preferences_description", "Choose your release channel, download source, behavior, and download speed.");
AutoCheckUpdatesLabel = L("settings.update.auto_check_toggle", "Automatically check for updates on startup");
- IncludePrereleaseUpdatesLabel = L("settings.update.include_prerelease_toggle", "Include prerelease versions");
UpdateChannelLabel = L("settings.update.channel_label", "Update Channel");
+ UpdateSourceLabel = L("settings.update.source_label", "Download Source");
+ UpdateModeLabel = L("settings.update.mode_label", "Update Mode");
+ DownloadThreadsLabel = L("settings.update.download_threads_label", "Download Threads");
+ DownloadThreadsDescription = L("settings.update.download_threads_desc", "Choose how many parallel download threads are used for application updates.");
CheckForUpdatesButtonText = L("settings.update.check_button", "Check for Updates");
+ DownloadButtonText = L("settings.update.download_install_button", "Download & Install");
+ InstallNowButtonText = L("settings.update.install_now_button", "Install Now");
+ CurrentVersionLabel = L("settings.update.current_version_label", "Current Version");
+ LatestVersionLabel = L("settings.update.latest_version_label", "Latest Release");
+ PublishedAtLabel = L("settings.update.published_at_label", "Published At");
+ LastCheckedLabel = L("settings.update.last_checked_label", "Last Checked");
+ StableChannelText = L("settings.update.channel_stable", "Stable");
+ PreviewChannelText = L("settings.update.channel_preview", "Preview");
+ GitHubSourceText = L("settings.update.source_github", "GitHub");
+ GhProxySourceText = L("settings.update.source_ghproxy", "gh-proxy");
+ ManualModeText = L("settings.update.mode_manual", "Manual Update");
+ DownloadThenConfirmModeText = L("settings.update.mode_download_then_confirm", "Silent Download");
+ SilentOnExitModeText = L("settings.update.mode_silent_on_exit", "Silent Install");
+ SelectedUpdateChannelDescription = BuildUpdateChannelDescription(SelectedUpdateChannelValue);
+ SelectedUpdateModeDescription = BuildUpdateModeDescription(SelectedUpdateModeValue);
+ SelectedUpdateSourceDescription = BuildUpdateSourceDescription(SelectedUpdateSourceValue);
+ }
+
+ private void LoadStateFromSettings()
+ {
+ var update = _settingsFacade.Update.Get();
+ _isInitializing = true;
+ AutoCheckUpdates = update.AutoCheckUpdates;
+ SelectedUpdateChannelValue = UpdateSettingsValues.NormalizeChannel(update.UpdateChannel, update.IncludePrereleaseUpdates);
+ SelectedUpdateSourceValue = UpdateSettingsValues.NormalizeDownloadSource(update.UpdateDownloadSource);
+ SelectedUpdateModeValue = UpdateSettingsValues.NormalizeMode(update.UpdateMode);
+ DownloadThreadsSliderValue = UpdateSettingsValues.NormalizeDownloadThreads(update.UpdateDownloadThreads);
+ DownloadThreadsText = ((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.CurrentCulture);
+ _isInitializing = false;
+
+ SyncSelectedOptions();
+ RefreshLastCheckedFromSettings();
+ ApplyPendingState(update);
+ if (!HasPendingInstaller)
+ {
+ UpdateStatus = L("settings.update.status_idle", "No update check has been performed yet.");
+ }
+
+ RefreshActionState();
+ }
+
+ private void RefreshLastCheckedFromSettings()
+ {
+ var update = _settingsFacade.Update.Get();
+ LastCheckedText = FormatTimestamp(update.LastUpdateCheckUtcMs);
+ IsLastCheckedVisible = !string.IsNullOrWhiteSpace(LastCheckedText);
+ }
+
+ private void ApplyPendingState(UpdateSettingsState update)
+ {
+ var pending = _updateWorkflowService.GetPendingUpdate();
+ HasPendingInstaller = pending is not null;
+ if (pending is null)
+ {
+ return;
+ }
+
+ LatestVersionText = pending.VersionText;
+ IsLatestVersionVisible = !string.IsNullOrWhiteSpace(LatestVersionText);
+ PublishedAtText = pending.PublishedAt is null ? string.Empty : FormatTimestamp(pending.PublishedAt.Value.ToUnixTimeMilliseconds());
+ IsPublishedAtVisible = !string.IsNullOrWhiteSpace(PublishedAtText);
+ UpdateStatus = BuildPendingReadyStatus();
+ }
+
+ private void ApplyCheckResultDisplay(UpdateCheckResult result)
+ {
+ if (result.IsUpdateAvailable)
+ {
+ LatestVersionText = result.LatestVersionText;
+ IsLatestVersionVisible = !string.IsNullOrWhiteSpace(LatestVersionText);
+ PublishedAtText = result.Release is null || result.Release.PublishedAt == DateTimeOffset.MinValue
+ ? string.Empty
+ : FormatTimestamp(result.Release.PublishedAt.ToUnixTimeMilliseconds());
+ IsPublishedAtVisible = !string.IsNullOrWhiteSpace(PublishedAtText);
+ return;
+ }
+
+ LatestVersionText = string.Empty;
+ PublishedAtText = string.Empty;
+ IsLatestVersionVisible = false;
+ IsPublishedAtVisible = false;
+ UpdateStatus = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_up_to_date_format", "You are up to date ({0})."),
+ result.CurrentVersionText);
+ }
+
+ private async Task DownloadLatestReleaseCoreAsync(UpdateCheckResult? result, bool invokedFromCheck)
+ {
+ if (result is null || !result.Success || !result.IsUpdateAvailable || result.PreferredAsset is null)
+ {
+ return;
+ }
+
+ try
+ {
+ IsDownloading = true;
+ IsDownloadProgressVisible = true;
+ DownloadProgressValue = 0;
+ DownloadProgressText = L("settings.update.download_progress_idle", "Download progress: -");
+ UpdateStatus = L("settings.update.status_downloading", "Downloading installer...");
+
+ var progress = new Progress(value =>
+ {
+ DownloadProgressValue = Math.Clamp(value * 100d, 0d, 100d);
+ DownloadProgressText = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.download_progress_format", "Download progress: {0:F0}%"),
+ DownloadProgressValue);
+ });
+
+ var downloadResult = await _updateWorkflowService.DownloadReleaseAsync(result, progress);
+ if (!downloadResult.Success)
+ {
+ UpdateStatus = string.Format(
+ CultureInfo.CurrentCulture,
+ L("settings.update.status_download_failed_format", "Download failed: {0}"),
+ downloadResult.ErrorMessage ?? L("settings.update.status_check_failed", "Failed to check for updates."));
+ return;
+ }
+
+ ApplyPendingState(_settingsFacade.Update.Get());
+ UpdateStatus = BuildPendingReadyStatus();
+ if (!invokedFromCheck)
+ {
+ _lastCheckResult = result;
+ }
+ }
+ finally
+ {
+ IsDownloading = false;
+ IsDownloadProgressVisible = false;
+ }
+ }
+
+ private string BuildPendingReadyStatus()
+ {
+ return string.Equals(SelectedUpdateModeValue, UpdateSettingsValues.ModeSilentOnExit, StringComparison.OrdinalIgnoreCase)
+ ? L("settings.update.status_downloaded_exit", "Update downloaded. It will be installed when you exit the app.")
+ : L("settings.update.status_downloaded_confirm", "Update downloaded. Review it and choose when to install.");
+ }
+
+ private string BuildUpdateModeDescription(string? value)
+ {
+ return UpdateSettingsValues.NormalizeMode(value) switch
+ {
+ UpdateSettingsValues.ModeManual => L(
+ "settings.update.mode_manual_desc",
+ "Only check for updates. You decide when downloads and installation happen."),
+ UpdateSettingsValues.ModeSilentOnExit => L(
+ "settings.update.mode_silent_on_exit_desc",
+ "Download updates in the background and install them the next time you exit the app."),
+ _ => L(
+ "settings.update.mode_download_then_confirm_desc",
+ "Download updates in the background and ask for confirmation before installing them.")
+ };
+ }
+
+ private string BuildUpdateChannelDescription(string? value)
+ {
+ return UpdateSettingsValues.NormalizeChannel(value) switch
+ {
+ UpdateSettingsValues.ChannelPreview => L(
+ "settings.update.channel_preview_desc",
+ "Preview builds may contain newer features but can be less stable."),
+ _ => L(
+ "settings.update.channel_stable_desc",
+ "Stable builds prioritize reliability and are recommended for most users.")
+ };
+ }
+
+ private string BuildUpdateSourceDescription(string? value)
+ {
+ return UpdateSettingsValues.NormalizeDownloadSource(value) switch
+ {
+ UpdateSettingsValues.DownloadSourceGhProxy => L(
+ "settings.update.source_ghproxy_desc",
+ "Use the gh-proxy mirror when downloading GitHub release assets."),
+ _ => L(
+ "settings.update.source_github_desc",
+ "Download release assets directly from GitHub.")
+ };
+ }
+
+ private string FormatTimestamp(long? utcMs)
+ {
+ if (utcMs is not > 0)
+ {
+ return string.Empty;
+ }
+
+ try
+ {
+ return DateTimeOffset
+ .FromUnixTimeMilliseconds(utcMs.Value)
+ .ToLocalTime()
+ .ToString("g", CultureInfo.CurrentCulture);
+ }
+ catch (ArgumentOutOfRangeException)
+ {
+ return string.Empty;
+ }
+ }
+
+ private void RefreshActionState()
+ {
+ OnPropertyChanged(nameof(IsDownloadButtonVisible));
+ OnPropertyChanged(nameof(IsInstallButtonVisible));
+ OnPropertyChanged(nameof(DownloadThreadsValueText));
+ }
+
+ private IReadOnlyList CreateUpdateChannelOptions()
+ {
+ return
+ [
+ new SelectionOption(UpdateSettingsValues.ChannelStable, StableChannelText),
+ new SelectionOption(UpdateSettingsValues.ChannelPreview, PreviewChannelText)
+ ];
+ }
+
+ private IReadOnlyList CreateUpdateSourceOptions()
+ {
+ return
+ [
+ new SelectionOption(UpdateSettingsValues.DownloadSourceGitHub, GitHubSourceText),
+ new SelectionOption(UpdateSettingsValues.DownloadSourceGhProxy, GhProxySourceText)
+ ];
+ }
+
+ private IReadOnlyList CreateUpdateModeOptions()
+ {
+ return
+ [
+ new SelectionOption(UpdateSettingsValues.ModeManual, ManualModeText),
+ new SelectionOption(UpdateSettingsValues.ModeDownloadThenConfirm, DownloadThenConfirmModeText),
+ new SelectionOption(UpdateSettingsValues.ModeSilentOnExit, SilentOnExitModeText)
+ ];
+ }
+
+ private IReadOnlyList CreateDownloadThreadOptions()
+ {
+ return Enumerable
+ .Range(UpdateSettingsValues.MinDownloadThreads, UpdateSettingsValues.MaxDownloadThreads)
+ .Select(value => new SelectionOption(
+ value.ToString(CultureInfo.InvariantCulture),
+ value.ToString(CultureInfo.CurrentCulture)))
+ .ToList();
+ }
+
+ private void SyncSelectedOptions()
+ {
+ SelectedUpdateChannelOption = UpdateChannelOptions.FirstOrDefault(option =>
+ string.Equals(option.Value, SelectedUpdateChannelValue, StringComparison.OrdinalIgnoreCase));
+ SelectedUpdateSourceOption = UpdateSourceOptions.FirstOrDefault(option =>
+ string.Equals(option.Value, SelectedUpdateSourceValue, StringComparison.OrdinalIgnoreCase));
+ SelectedUpdateModeOption = UpdateModeOptions.FirstOrDefault(option =>
+ string.Equals(option.Value, SelectedUpdateModeValue, StringComparison.OrdinalIgnoreCase));
+ SelectedDownloadThreadsOption = DownloadThreadOptions.FirstOrDefault(option =>
+ string.Equals(
+ option.Value,
+ UpdateSettingsValues.NormalizeDownloadThreads((int)Math.Round(DownloadThreadsSliderValue)).ToString(CultureInfo.InvariantCulture),
+ StringComparison.OrdinalIgnoreCase));
+ }
+
+ private void ApplyDownloadThreadsValue(int value, bool saveChanges)
+ {
+ var normalized = UpdateSettingsValues.NormalizeDownloadThreads(value);
+ var normalizedText = normalized.ToString(CultureInfo.CurrentCulture);
+
+ var previousInitializing = _isInitializing;
+ _isInitializing = true;
+ DownloadThreadsSliderValue = normalized;
+ DownloadThreadsText = normalizedText;
+ _isInitializing = previousInitializing;
+ SyncSelectedOptions();
+
+ if (saveChanges)
+ {
+ SaveUpdateSettings();
+ UpdateStatus = L("settings.update.status_preferences_saved", "Update preferences saved.");
+ }
+ }
+
+ private static bool TryParseDownloadThreads(string? value, out int parsed)
+ {
+ if (int.TryParse(value, NumberStyles.Integer, CultureInfo.CurrentCulture, out parsed))
+ {
+ return true;
+ }
+
+ return int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out parsed);
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs
new file mode 100644
index 0000000..91d4f66
--- /dev/null
+++ b/LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs
@@ -0,0 +1,214 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using CommunityToolkit.Mvvm.ComponentModel;
+using LanMountainDesktop.ComponentSystem;
+using LanMountainDesktop.Services;
+using LanMountainDesktop.Services.Settings;
+
+namespace LanMountainDesktop.ViewModels;
+
+public sealed partial class StatusBarSettingsPageViewModel : ViewModelBase
+{
+ private readonly ISettingsFacadeService _settingsFacade;
+ private readonly LocalizationService _localizationService = new();
+ private readonly string _languageCode;
+ private bool _isInitializing;
+
+ public StatusBarSettingsPageViewModel(ISettingsFacadeService settingsFacade)
+ {
+ _settingsFacade = settingsFacade;
+ _languageCode = _localizationService.NormalizeLanguageCode(_settingsFacade.Region.Get().LanguageCode);
+
+ ClockFormats = CreateClockFormats();
+ SpacingModes = CreateSpacingModes();
+ RefreshLocalizedText();
+
+ _isInitializing = true;
+ Load();
+ _isInitializing = false;
+ }
+
+ public IReadOnlyList ClockFormats { get; }
+
+ public IReadOnlyList SpacingModes { get; }
+
+ [ObservableProperty]
+ private bool _showClock = true;
+
+ [ObservableProperty]
+ private SelectionOption _selectedClockFormat = new("HourMinuteSecond", "Hour:Minute:Second");
+
+ [ObservableProperty]
+ private SelectionOption _selectedSpacingMode = new("Relaxed", "Relaxed");
+
+ [ObservableProperty]
+ private int _customSpacingPercent = 12;
+
+ [ObservableProperty]
+ private bool _isCustomSpacingVisible;
+
+ [ObservableProperty]
+ private string _componentsHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _pageTitle = string.Empty;
+
+ [ObservableProperty]
+ private string _pageDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _clockHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _clockDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _clockFormatLabel = string.Empty;
+
+ [ObservableProperty]
+ private string _spacingHeader = string.Empty;
+
+ [ObservableProperty]
+ private string _spacingDescription = string.Empty;
+
+ [ObservableProperty]
+ private string _customSpacingLabel = string.Empty;
+
+ public void Load()
+ {
+ var state = _settingsFacade.StatusBar.Get();
+
+ ShowClock = state.TopStatusComponentIds.Any(id =>
+ string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase));
+
+ var clockFormat = string.IsNullOrWhiteSpace(state.ClockDisplayFormat)
+ ? "HourMinuteSecond"
+ : state.ClockDisplayFormat;
+ SelectedClockFormat = ClockFormats.FirstOrDefault(option =>
+ string.Equals(option.Value, clockFormat, StringComparison.OrdinalIgnoreCase))
+ ?? ClockFormats[1];
+
+ var spacingMode = NormalizeSpacingMode(state.SpacingMode);
+ SelectedSpacingMode = SpacingModes.FirstOrDefault(option =>
+ string.Equals(option.Value, spacingMode, StringComparison.OrdinalIgnoreCase))
+ ?? SpacingModes[1];
+ CustomSpacingPercent = Math.Clamp(state.CustomSpacingPercent, 0, 30);
+ IsCustomSpacingVisible = string.Equals(SelectedSpacingMode.Value, "Custom", StringComparison.OrdinalIgnoreCase);
+ }
+
+ partial void OnShowClockChanged(bool value)
+ {
+ if (_isInitializing)
+ {
+ return;
+ }
+
+ Save();
+ }
+
+ partial void OnSelectedClockFormatChanged(SelectionOption value)
+ {
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ Save();
+ }
+
+ partial void OnSelectedSpacingModeChanged(SelectionOption value)
+ {
+ IsCustomSpacingVisible = string.Equals(value?.Value, "Custom", StringComparison.OrdinalIgnoreCase);
+ if (_isInitializing || value is null)
+ {
+ return;
+ }
+
+ Save();
+ }
+
+ partial void OnCustomSpacingPercentChanged(int value)
+ {
+ var normalized = Math.Clamp(value, 0, 30);
+ if (normalized != value)
+ {
+ CustomSpacingPercent = normalized;
+ return;
+ }
+
+ if (_isInitializing || !IsCustomSpacingVisible)
+ {
+ return;
+ }
+
+ Save();
+ }
+
+ private void Save()
+ {
+ var state = _settingsFacade.StatusBar.Get();
+ var topComponents = state.TopStatusComponentIds
+ .Where(id => !string.Equals(id, BuiltInComponentIds.Clock, StringComparison.OrdinalIgnoreCase))
+ .ToList();
+
+ if (ShowClock)
+ {
+ topComponents.Add(BuiltInComponentIds.Clock);
+ }
+
+ _settingsFacade.StatusBar.Save(new StatusBarSettingsState(
+ topComponents,
+ state.PinnedTaskbarActions,
+ state.EnableDynamicTaskbarActions,
+ state.TaskbarLayoutMode,
+ SelectedClockFormat.Value,
+ NormalizeSpacingMode(SelectedSpacingMode.Value),
+ Math.Clamp(CustomSpacingPercent, 0, 30)));
+ }
+
+ private IReadOnlyList CreateClockFormats()
+ {
+ return
+ [
+ new SelectionOption("HourMinute", L("settings.status_bar.clock_format.hm", "Hour:Minute")),
+ new SelectionOption("HourMinuteSecond", L("settings.status_bar.clock_format.hms", "Hour:Minute:Second"))
+ ];
+ }
+
+ private IReadOnlyList CreateSpacingModes()
+ {
+ return
+ [
+ new SelectionOption("Compact", L("settings.status_bar.spacing_mode_compact", "Compact")),
+ new SelectionOption("Relaxed", L("settings.status_bar.spacing_mode_relaxed", "Relaxed")),
+ new SelectionOption("Custom", L("settings.status_bar.spacing_mode_custom", "Custom"))
+ ];
+ }
+
+ private void RefreshLocalizedText()
+ {
+ PageTitle = L("settings.status_bar.title", "Status Bar");
+ PageDescription = L("settings.status_bar.description", "Choose which single-height components appear on the top status bar.");
+ ComponentsHeader = L("settings.status_bar.title", "Status Bar");
+ ClockHeader = L("settings.status_bar.clock_header", "Clock Component");
+ ClockDescription = L("settings.status_bar.clock_description", "Display a clock on the top status bar.");
+ ClockFormatLabel = L("settings.status_bar.clock_format_label", "Clock format");
+ SpacingHeader = L("settings.status_bar.spacing_header", "Component Spacing");
+ SpacingDescription = L("settings.status_bar.spacing_desc", "Adjust spacing between status bar components.");
+ CustomSpacingLabel = L("settings.status_bar.spacing_custom_label", "Custom spacing (%)");
+ }
+
+ private string NormalizeSpacingMode(string? value)
+ {
+ return value switch
+ {
+ _ when string.Equals(value, "Compact", StringComparison.OrdinalIgnoreCase) => "Compact",
+ _ when string.Equals(value, "Custom", StringComparison.OrdinalIgnoreCase) => "Custom",
+ _ => "Relaxed"
+ };
+ }
+
+ private string L(string key, string fallback)
+ => _localizationService.GetString(_languageCode, key, fallback);
+}
diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
index 398c6cc..4581d37 100644
--- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
+++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs
@@ -198,11 +198,16 @@ public partial class MainWindow
private ThemeColorContext BuildAdaptiveThemeContext()
{
+ var palette = _themeSettingsService.BuildPalette(_isNightMode, _wallpaperPath, _selectedThemeColor.ToString());
+ var accentColor = palette.MonetColors is { Count: > 0 }
+ ? palette.MonetColors[0]
+ : _selectedThemeColor;
return new ThemeColorContext(
- _selectedThemeColor,
+ accentColor,
IsLightBackground: !_isNightMode,
IsLightNavBackground: !_isNightMode,
- IsNightMode: _isNightMode);
+ IsNightMode: _isNightMode,
+ MonetColors: palette.MonetColors);
}
private void ApplyAdaptiveThemeResources()
@@ -386,7 +391,7 @@ public partial class MainWindow
return;
}
- var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath);
+ var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath, _selectedThemeColor.ToString());
_recommendedColors = palette.RecommendedColors;
_monetColors = palette.MonetColors;
}
@@ -406,6 +411,32 @@ public partial class MainWindow
private void TriggerAutoUpdateCheckIfEnabled()
{
+ var versionText = _settingsFacade.ApplicationInfo.GetAppVersionText();
+ if (!Version.TryParse(versionText, out var currentVersion))
+ {
+ currentVersion = new Version(0, 0, 0);
+ }
+
+ var normalizedVersion = new Version(
+ Math.Max(0, currentVersion.Major),
+ Math.Max(0, currentVersion.Minor),
+ Math.Max(0, currentVersion.Build));
+
+ DispatcherTimer.RunOnce(
+ async () =>
+ {
+ try
+ {
+ await HostUpdateWorkflowServiceProvider
+ .GetOrCreate()
+ .AutoCheckIfEnabledAsync(normalizedVersion);
+ }
+ catch (Exception ex)
+ {
+ AppLogger.Warn("UpdateWorkflow", "Automatic update check failed after startup.", ex);
+ }
+ },
+ TimeSpan.FromSeconds(3));
}
private void PersistSettings()
@@ -489,6 +520,7 @@ public partial class MainWindow
private AppSettingsSnapshot BuildAppSettingsSnapshot()
{
var latestWeatherState = _weatherSettingsService.Get();
+ var latestUpdateState = _updateSettingsService.Get();
return new AppSettingsSnapshot
{
GridShortSideCells = _targetShortSideCells,
@@ -513,6 +545,16 @@ public partial class MainWindow
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
AutoStartWithWindows = _autoStartWithWindows,
AppRenderMode = _selectedAppRenderMode,
+ AutoCheckUpdates = latestUpdateState.AutoCheckUpdates,
+ IncludePrereleaseUpdates = latestUpdateState.IncludePrereleaseUpdates,
+ UpdateChannel = latestUpdateState.UpdateChannel,
+ UpdateMode = latestUpdateState.UpdateMode,
+ UpdateDownloadSource = latestUpdateState.UpdateDownloadSource,
+ UpdateDownloadThreads = latestUpdateState.UpdateDownloadThreads,
+ PendingUpdateInstallerPath = latestUpdateState.PendingUpdateInstallerPath,
+ PendingUpdateVersion = latestUpdateState.PendingUpdateVersion,
+ PendingUpdatePublishedAtUtcMs = latestUpdateState.PendingUpdatePublishedAtUtcMs,
+ LastUpdateCheckUtcMs = latestUpdateState.LastUpdateCheckUtcMs,
TopStatusComponentIds = [.. _topStatusComponentIds],
PinnedTaskbarActions = [.. _pinnedTaskbarActions.Select(v => v.ToString())],
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
index 75e9117..5ccc7c9 100644
--- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml
@@ -6,96 +6,71 @@
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
x:DataType="vm:AboutSettingsPageViewModel">
-
-
+
+
-
-
+
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs
index ef26417..395d855 100644
--- a/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsPages/AboutSettingsPage.axaml.cs
@@ -1,3 +1,6 @@
+using System;
+using Avalonia;
+using Avalonia.Controls;
using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels;
@@ -11,9 +14,12 @@ namespace LanMountainDesktop.Views.SettingsPages;
IconKey = "Info",
SortOrder = 40,
TitleLocalizationKey = "settings.about.title",
- DescriptionLocalizationKey = "settings.about.description")]
+ DescriptionLocalizationKey = "settings.about.description",
+ HidePageTitle = true)]
public partial class AboutSettingsPage : SettingsPageBase
{
+ private const double HeroAspectRatio = 9d / 16d;
+
public AboutSettingsPage()
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{
@@ -24,7 +30,34 @@ public partial class AboutSettingsPage : SettingsPageBase
ViewModel = viewModel;
DataContext = ViewModel;
InitializeComponent();
+ if (AboutHeroCard is not null)
+ {
+ AboutHeroCard.SizeChanged += OnAboutHeroCardSizeChanged;
+ UpdateHeroCardHeight(AboutHeroCard.Bounds.Width);
+ }
}
public AboutSettingsPageViewModel ViewModel { get; }
+
+ private void OnAboutHeroCardSizeChanged(object? sender, SizeChangedEventArgs e)
+ {
+ _ = sender;
+ UpdateHeroCardHeight(e.NewSize.Width);
+ }
+
+ private void UpdateHeroCardHeight(double width)
+ {
+ if (AboutHeroCard is null || width <= 1d)
+ {
+ return;
+ }
+
+ var targetHeight = Math.Round(width * HeroAspectRatio, 2);
+ if (Math.Abs(AboutHeroCard.Height - targetHeight) <= 0.5d)
+ {
+ return;
+ }
+
+ AboutHeroCard.Height = targetHeight;
+ }
}
diff --git a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
index e3ee8d6..9f68dbd 100644
--- a/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
+++ b/LanMountainDesktop/Views/SettingsPages/AppearanceSettingsPage.axaml
@@ -39,39 +39,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml
new file mode 100644
index 0000000..6b43d84
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml.cs
new file mode 100644
index 0000000..1c47bc3
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/StatusBarSettingsPage.axaml.cs
@@ -0,0 +1,30 @@
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.ViewModels;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+[SettingsPageInfo(
+ "status-bar",
+ "Status Bar",
+ SettingsPageCategory.Appearance,
+ IconKey = "Apps",
+ SortOrder = 17,
+ TitleLocalizationKey = "settings.status_bar.title",
+ DescriptionLocalizationKey = "settings.status_bar.description")]
+public partial class StatusBarSettingsPage : SettingsPageBase
+{
+ public StatusBarSettingsPage()
+ : this(new StatusBarSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
+ {
+ }
+
+ public StatusBarSettingsPage(StatusBarSettingsPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+
+ public StatusBarSettingsPageViewModel ViewModel { get; }
+}
diff --git a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
new file mode 100644
index 0000000..871fcae
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
@@ -0,0 +1,224 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml.cs b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml.cs
new file mode 100644
index 0000000..ad2046b
--- /dev/null
+++ b/LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml.cs
@@ -0,0 +1,30 @@
+using LanMountainDesktop.PluginSdk;
+using LanMountainDesktop.Services.Settings;
+using LanMountainDesktop.ViewModels;
+
+namespace LanMountainDesktop.Views.SettingsPages;
+
+[SettingsPageInfo(
+ "update",
+ "Update",
+ SettingsPageCategory.About,
+ IconKey = "ArrowSync",
+ SortOrder = 35,
+ TitleLocalizationKey = "settings.update.title",
+ DescriptionLocalizationKey = "settings.update.description")]
+public partial class UpdateSettingsPage : SettingsPageBase
+{
+ public UpdateSettingsPage()
+ : this(new UpdateSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
+ {
+ }
+
+ public UpdateSettingsPage(UpdateSettingsPageViewModel viewModel)
+ {
+ ViewModel = viewModel;
+ DataContext = ViewModel;
+ InitializeComponent();
+ }
+
+ public UpdateSettingsPageViewModel ViewModel { get; }
+}
diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
index 20a603f..14ff84e 100644
--- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs
@@ -216,8 +216,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.CurrentPageTitle = descriptor.Title;
ViewModel.CurrentPageDescription = descriptor.Description;
ViewModel.CurrentPageId = descriptor.PageId;
+ ViewModel.IsPageTitleVisible = !descriptor.HidePageTitle;
TrySelectNavigationItem(descriptor.PageId);
SyncTitleText();
+ UpdateResponsiveLayout();
+ RequestResponsiveLayoutRefresh();
}
private SettingsPageDescriptor? ResolveDescriptor(string? pageId)