settings_re10

This commit is contained in:
lincube
2026-03-15 04:35:34 +08:00
parent 85b70c4a8a
commit c7fb48c8ee
28 changed files with 2294 additions and 349 deletions

View File

@@ -25,6 +25,8 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
outputs: outputs:
version: ${{ steps.version.outputs.version }} version: ${{ steps.version.outputs.version }}
assembly_version: ${{ steps.version.outputs.assembly_version }}
informational_version: ${{ steps.version.outputs.informational_version }}
tag: ${{ steps.version.outputs.tag }} tag: ${{ steps.version.outputs.tag }}
checkout_ref: ${{ steps.version.outputs.checkout_ref }} checkout_ref: ${{ steps.version.outputs.checkout_ref }}
@@ -47,8 +49,15 @@ jobs:
CHECKOUT_REF="${GITHUB_SHA}" CHECKOUT_REF="${GITHUB_SHA}"
fi fi
VERSION="${TAG#v}" 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 "tag=${TAG}" >> $GITHUB_OUTPUT
echo "version=${VERSION}" >> $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 echo "checkout_ref=${CHECKOUT_REF}" >> $GITHUB_OUTPUT
build-windows: build-windows:
@@ -73,26 +82,16 @@ jobs:
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} 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>.*?</Version>', "<Version>$VERSION</Version>"
Set-Content $csprojPath $content
}
shell: pwsh
- name: Restore - name: Restore
run: dotnet restore ${{ env.Solution_Name }} run: dotnet restore ${{ env.Solution_Name }}
- name: Build - 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 - name: Publish
run: | run: |
@@ -106,7 +105,11 @@ jobs:
-p:DebugType=none ` -p:DebugType=none `
-p:DebugSymbols=false ` -p:DebugSymbols=false `
-p:PublishTrimmed=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 shell: pwsh
- name: Install Inno Setup - name: Install Inno Setup
@@ -242,17 +245,16 @@ jobs:
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} 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>$VERSION<\/Version>/" LanMountainDesktop/LanMountainDesktop.csproj
- name: Restore - name: Restore
run: dotnet restore ${{ env.Solution_Name }} run: dotnet restore ${{ env.Solution_Name }}
- name: Build - 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 - name: Publish
run: | run: |
@@ -266,7 +268,11 @@ jobs:
-p:DebugType=none \ -p:DebugType=none \
-p:DebugSymbols=false \ -p:DebugSymbols=false \
-p:PublishTrimmed=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 - name: Package as DEB
run: | run: |
@@ -384,17 +390,16 @@ jobs:
with: with:
dotnet-version: ${{ env.DOTNET_VERSION }} 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>$VERSION<\/Version>/" LanMountainDesktop/LanMountainDesktop.csproj
- name: Restore - name: Restore
run: dotnet restore ${{ env.Solution_Name }} run: dotnet restore ${{ env.Solution_Name }}
- name: Build - 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 - name: Publish
run: | run: |
@@ -408,7 +413,11 @@ jobs:
-p:DebugType=none \ -p:DebugType=none \
-p:DebugSymbols=false \ -p:DebugSymbols=false \
-p:PublishTrimmed=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 - name: Package as DMG
run: | run: |

View File

@@ -493,7 +493,10 @@ public partial class App : Application
var themeChanged = var themeChanged =
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || 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 = var languageChanged =
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase); changedKeys.Contains(nameof(AppSettingsSnapshot.LanguageCode), StringComparer.OrdinalIgnoreCase);
@@ -516,16 +519,32 @@ public partial class App : Application
private void ApplyAdaptiveThemeResources(ThemeAppearanceSettingsState themeState) 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( var context = new ThemeColorContext(
accentColor, accentColor,
IsLightBackground: !themeState.IsNightMode, IsLightBackground: !themeState.IsNightMode,
IsLightNavBackground: !themeState.IsNightMode, IsLightNavBackground: !themeState.IsNightMode,
IsNightMode: themeState.IsNightMode); IsNightMode: themeState.IsNightMode,
MonetColors: monetPalette.MonetColors);
ThemeColorSystemService.ApplyThemeResources(Resources, context); ThemeColorSystemService.ApplyThemeResources(Resources, context);
GlassEffectService.ApplyGlassResources(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) private static Color TryParseThemeColor(string? colorText)
{ {
if (!string.IsNullOrWhiteSpace(colorText)) if (!string.IsNullOrWhiteSpace(colorText))
@@ -589,6 +608,14 @@ public partial class App : Application
_exitCleanupCompleted = true; _exitCleanupCompleted = true;
AppSettingsService.SettingsSaved -= OnAppSettingsSaved; AppSettingsService.SettingsSaved -= OnAppSettingsSaved;
_settingsFacade.Settings.Changed -= OnSettingsChanged; _settingsFacade.Settings.Changed -= OnSettingsChanged;
try
{
HostUpdateWorkflowServiceProvider.GetOrCreate().TryApplyPendingUpdateOnExit();
}
catch (Exception ex)
{
AppLogger.Warn("UpdateWorkflow", "Failed to apply pending update during exit cleanup.", ex);
}
try try
{ {

Binary file not shown.

After

Width:  |  Height:  |  Size: 999 KiB

View File

@@ -242,7 +242,7 @@
"settings.general.preview_date_label": "Date", "settings.general.preview_date_label": "Date",
"settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.", "settings.general.render_mode_restart_message": "Rendering mode changes require restarting the app.",
"settings.appearance.title": "Appearance", "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.appearance.theme_header": "Theme",
"settings.color.enable_night_mode_toggle": "Enable night mode", "settings.color.enable_night_mode_toggle": "Enable night mode",
"settings.color.use_system_chrome_toggle": "Use system window chrome", "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.current_format": "Current backend: {0}",
"settings.about.render_mode.impl_format": "Runtime implementation: {0}", "settings.about.render_mode.impl_format": "Runtime implementation: {0}",
"settings.about.render_mode.impl_unavailable": "Runtime implementation details are unavailable.", "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.app_info_header": "Application Information",
"settings.about.update_header": "Updates", "settings.about.update_header": "Updates",
"settings.about.version_label": "Version", "settings.about.version_label": "Version",
"settings.about.codename_label": "Codename",
"settings.about.render_backend_label": "Render Backend", "settings.about.render_backend_label": "Render Backend",
"settings.about.render_backend_format": "Render Backend: {0}", "settings.about.render_backend_format": "Render Backend: {0}",
"settings.restart_dialog.title": "Restart required", "settings.restart_dialog.title": "Restart required",

View File

@@ -247,7 +247,7 @@
"settings.general.preview_date_label": "日期", "settings.general.preview_date_label": "日期",
"settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。", "settings.general.render_mode_restart_message": "渲染模式变更需要重启应用。",
"settings.appearance.title": "外观", "settings.appearance.title": "外观",
"settings.appearance.description": "切换主题与状态栏展示。", "settings.appearance.description": "调整主题、壁纸与窗口外观。",
"settings.appearance.theme_header": "主题", "settings.appearance.theme_header": "主题",
"settings.color.enable_night_mode_toggle": "启用夜间模式", "settings.color.enable_night_mode_toggle": "启用夜间模式",
"settings.color.use_system_chrome_toggle": "使用系统窗口标题栏", "settings.color.use_system_chrome_toggle": "使用系统窗口标题栏",
@@ -315,10 +315,36 @@
"settings.about.render_mode.current_format": "当前后端:{0}", "settings.about.render_mode.current_format": "当前后端:{0}",
"settings.about.render_mode.impl_format": "运行时实现:{0}", "settings.about.render_mode.impl_format": "运行时实现:{0}",
"settings.about.render_mode.impl_unavailable": "当前无法获取运行时实现信息。", "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.app_info_header": "应用信息",
"settings.about.update_header": "更新", "settings.about.update_header": "更新",
"settings.about.version_label": "版本", "settings.about.version_label": "版本",
"settings.about.codename_label": "版本代号",
"settings.about.render_backend_label": "渲染后端", "settings.about.render_backend_label": "渲染后端",
"settings.about.render_backend_format": "渲染后端:{0}", "settings.about.render_backend_format": "渲染后端:{0}",
"settings.restart_dialog.title": "需要重启应用", "settings.restart_dialog.title": "需要重启应用",

View File

@@ -60,7 +60,21 @@ public sealed class AppSettingsSnapshot
public bool IncludePrereleaseUpdates { get; set; } 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<string> TopStatusComponentIds { get; set; } = []; public List<string> TopStatusComponentIds { get; set; } = [];

View File

@@ -124,7 +124,10 @@ internal sealed class ComponentEditorWindowService : IComponentEditorWindowServi
var themeState = _settingsFacade.Theme.Get(); var themeState = _settingsFacade.Theme.Get();
var wallpaperState = _settingsFacade.Wallpaper.Get(); var wallpaperState = _settingsFacade.Wallpaper.Get();
var wallpaperMediaType = _settingsFacade.WallpaperMedia.DetectMediaType(wallpaperState.WallpaperPath); 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( var palette = ComponentEditorMaterialThemeAdapter.Build(
themeState, themeState,
wallpaperState, wallpaperState,

View File

@@ -172,6 +172,8 @@ public sealed class GitHubReleaseUpdateService : IDisposable
public async Task<UpdateDownloadResult> DownloadAssetAsync( public async Task<UpdateDownloadResult> DownloadAssetAsync(
GitHubReleaseAsset asset, GitHubReleaseAsset asset,
string destinationFilePath, string destinationFilePath,
string downloadSource,
int maxParallelSegments,
IProgress<double>? progress = null, IProgress<double>? progress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
@@ -193,11 +195,14 @@ public sealed class GitHubReleaseUpdateService : IDisposable
var progressAdapter = progress is null var progressAdapter = progress is null
? null ? null
: new Progress<DownloadProgressInfo>(info => progress.Report(info.Progress)); : new Progress<DownloadProgressInfo>(info => progress.Report(info.Progress));
var effectiveSource = ApplyDownloadSource(asset.BrowserDownloadUrl, downloadSource);
var result = await _downloadService.DownloadAsync( var result = await _downloadService.DownloadAsync(
asset.BrowserDownloadUrl, effectiveSource,
destinationFilePath, destinationFilePath,
new DownloadOptions(ExpectedSizeBytes: asset.SizeBytes > 0 ? asset.SizeBytes : null), new DownloadOptions(
ExpectedSizeBytes: asset.SizeBytes > 0 ? asset.SizeBytes : null,
MaxParallelSegments: UpdateSettingsValues.NormalizeDownloadThreads(maxParallelSegments)),
progressAdapter, progressAdapter,
cancellationToken); cancellationToken);
@@ -460,4 +465,23 @@ public sealed class GitHubReleaseUpdateService : IDisposable
return value[..maxLength]; 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;
}
} }

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls; using System.Linq;
using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
@@ -6,63 +7,59 @@ namespace LanMountainDesktop.Services;
public static class GlassEffectService 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) public static void ApplyGlassResources(IResourceDictionary resources, ThemeColorContext context)
{ {
// Mica 材质:不透明,但混合壁纸颜色 var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
// 提取壁纸颜色的透明度0-1用于控制 Mica 效果强度 var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
var wallpaperTintOpacity = 0.15; // 壁纸颜色混合比例 var secondary = monetColors.Length > 1
? monetColors[1]
: ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12);
var neutralBase = context.IsNightMode ? Color.Parse("#FF202020") : Color.Parse("#FFF3F3F3"); var panelBase = context.IsNightMode
var neutralElevated = context.IsNightMode ? Color.Parse("#FF2C2C2C") : Color.Parse("#FFFAFAFA"); ? 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);
// Mica 效果:将壁纸颜色混合到中性基色中 var buttonBackground = Color.FromArgb(
var micaBackground = ColorMath.Blend(neutralBase, context.AccentColor, wallpaperTintOpacity); context.IsNightMode ? (byte)0x4D : (byte)0x52,
var micaElevated = ColorMath.Blend(neutralElevated, context.AccentColor, wallpaperTintOpacity * 0.8); panelRaised.R,
panelRaised.G,
// 按钮颜色 panelRaised.B);
var buttonBackground = context.IsNightMode ? var buttonBorder = Color.FromArgb(
Color.FromArgb(0x33, micaBackground.R, micaBackground.G, micaBackground.B) : context.IsNightMode ? (byte)0x36 : (byte)0x26,
Color.FromArgb(0x4D, micaBackground.R, micaBackground.G, micaBackground.B); primary.R,
primary.G,
primary.B);
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground); resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush( resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
Color.FromArgb(0x1A, neutralElevated.R, neutralElevated.G, neutralElevated.B));
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush( 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( 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( resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xF0 : (byte)0xF8, Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B));
micaBackground.R, micaBackground.G, micaBackground.B));
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush( 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( resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xF4 : (byte)0xFB, Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B));
micaElevated.R, micaElevated.G, micaElevated.B));
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush( 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( resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF2, Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B));
micaBackground.R, micaBackground.G, micaBackground.B));
// 模糊半径Mica 不需要强模糊) resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 22.0 : 28.0;
resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 20.0 : 30.0; resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0;
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 25.0 : 35.0; resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0;
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 30.0 : 40.0; resources["AdaptiveGlassPanelOpacity"] = 1.0;
resources["AdaptiveGlassStrongOpacity"] = 1.0;
// 不透明度Mica 材质接近不透明) resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.95 : 0.98;
resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.99 : 1.0; resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.012 : 0.008;
resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 1.0 : 1.0;
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.94 : 0.97;
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.01 : 0.008;
} }
} }

View File

@@ -12,10 +12,10 @@ namespace LanMountainDesktop.Services;
public sealed class MonetColorService 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 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); var monet = BuildMonetPalette(seed, nightMode);
return new MonetPalette(recommended, monet); return new MonetPalette(recommended, monet);
} }

View File

@@ -37,7 +37,17 @@ public sealed record WeatherSettingsState(
bool NoTlsRequests, bool NoTlsRequests,
string LocationQuery); string LocationQuery);
public sealed record RegionSettingsState(string LanguageCode, string? TimeZoneId); 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<string> DisabledPluginIds); public sealed record PluginManagementSettingsState(IReadOnlyList<string> DisabledPluginIds);
public sealed record PluginMarketDependencyInfo( public sealed record PluginMarketDependencyInfo(
string Id, string Id,
@@ -106,7 +116,7 @@ public interface IThemeAppearanceService
{ {
ThemeAppearanceSettingsState Get(); ThemeAppearanceSettingsState Get();
void Save(ThemeAppearanceSettingsState state); void Save(ThemeAppearanceSettingsState state);
MonetPalette BuildPalette(bool nightMode, string? wallpaperPath); MonetPalette BuildPalette(bool nightMode, string? wallpaperPath, string? preferredSeedColor = null);
} }
public interface IStatusBarSettingsService public interface IStatusBarSettingsService
@@ -164,6 +174,8 @@ public interface IUpdateSettingsService
Task<UpdateDownloadResult> DownloadAssetAsync( Task<UpdateDownloadResult> DownloadAssetAsync(
GitHubReleaseAsset asset, GitHubReleaseAsset asset,
string destinationFilePath, string destinationFilePath,
string downloadSource,
int maxParallelSegments,
IProgress<double>? progress = null, IProgress<double>? progress = null,
CancellationToken cancellationToken = default); CancellationToken cancellationToken = default);
} }
@@ -197,6 +209,7 @@ public interface IPluginMarketSettingsService
public interface IApplicationInfoService public interface IApplicationInfoService
{ {
string GetAppVersionText(); string GetAppVersionText();
string GetAppCodenameText();
AppRenderBackendInfo GetRenderBackendInfo(); AppRenderBackendInfo GetRenderBackendInfo();
} }

View File

@@ -2,8 +2,10 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using LanMountainDesktop.Models; using LanMountainDesktop.Models;
using LanMountainDesktop.PluginSdk; 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; Bitmap? bitmap = null;
Color? preferredSeed = null;
if (!string.IsNullOrWhiteSpace(preferredSeedColor) && Color.TryParse(preferredSeedColor, out var parsedSeed))
{
preferredSeed = parsedSeed;
}
try try
{ {
@@ -274,7 +282,7 @@ internal sealed class ThemeAppearanceService : IThemeAppearanceService
try try
{ {
return _monetColorService.BuildPalette(bitmap, nightMode); return _monetColorService.BuildPalette(bitmap, nightMode, preferredSeed);
} }
finally finally
{ {
@@ -530,18 +538,49 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
public UpdateSettingsState Get() public UpdateSettingsState Get()
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
snapshot.UpdateChannel,
snapshot.IncludePrereleaseUpdates);
return new UpdateSettingsState( return new UpdateSettingsState(
snapshot.AutoCheckUpdates, snapshot.AutoCheckUpdates,
snapshot.IncludePrereleaseUpdates, string.Equals(normalizedChannel, UpdateSettingsValues.ChannelPreview, StringComparison.OrdinalIgnoreCase),
snapshot.UpdateChannel); 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) public void Save(UpdateSettingsState state)
{ {
var snapshot = _settingsService.Load(); var snapshot = _settingsService.Load();
var normalizedChannel = UpdateSettingsValues.NormalizeChannel(
state.UpdateChannel,
state.IncludePrereleaseUpdates);
snapshot.AutoCheckUpdates = state.AutoCheckUpdates; snapshot.AutoCheckUpdates = state.AutoCheckUpdates;
snapshot.IncludePrereleaseUpdates = state.IncludePrereleaseUpdates; snapshot.IncludePrereleaseUpdates = string.Equals(
snapshot.UpdateChannel = state.UpdateChannel; 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( _settingsService.SaveSnapshot(
SettingsScope.App, SettingsScope.App,
snapshot, snapshot,
@@ -549,7 +588,14 @@ internal sealed class UpdateSettingsService : IUpdateSettingsService, IDisposabl
[ [
nameof(AppSettingsSnapshot.AutoCheckUpdates), nameof(AppSettingsSnapshot.AutoCheckUpdates),
nameof(AppSettingsSnapshot.IncludePrereleaseUpdates), 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<UpdateDownloadResult> DownloadAssetAsync( public Task<UpdateDownloadResult> DownloadAssetAsync(
GitHubReleaseAsset asset, GitHubReleaseAsset asset,
string destinationFilePath, string destinationFilePath,
string downloadSource,
int maxParallelSegments,
IProgress<double>? progress = null, IProgress<double>? progress = null,
CancellationToken cancellationToken = default) CancellationToken cancellationToken = default)
{ {
return _releaseUpdateService.DownloadAssetAsync(asset, destinationFilePath, progress, cancellationToken); return _releaseUpdateService.DownloadAssetAsync(
asset,
destinationFilePath,
downloadSource,
maxParallelSegments,
progress,
cancellationToken);
} }
public void Dispose() public void Dispose()
@@ -795,15 +849,50 @@ internal sealed class PluginMarketSettingsService : IPluginMarketSettingsService
internal sealed class ApplicationInfoService : IApplicationInfoService internal sealed class ApplicationInfoService : IApplicationInfoService
{ {
private const string Codename = "Administrate";
public string GetAppVersionText() public string GetAppVersionText()
{ {
var version = typeof(App).Assembly.GetName().Version; var assembly = typeof(App).Assembly;
return version is null var informationalVersion = assembly
? "0.0.0" .GetCustomAttribute<AssemblyInformationalVersionAttribute>()?
: new Version( .InformationalVersion;
Math.Max(0, version.Major), if (!string.IsNullOrWhiteSpace(informationalVersion))
Math.Max(0, version.Minor), {
Math.Max(0, version.Build)).ToString(3); 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() public AppRenderBackendInfo GetRenderBackendInfo()

View File

@@ -281,6 +281,9 @@ internal sealed class SettingsWindowService : ISettingsWindowService
refreshAll || refreshAll ||
changedKeys.Contains(nameof(AppSettingsSnapshot.IsNightMode), StringComparer.OrdinalIgnoreCase) || 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) ||
changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase); changedKeys.Contains(nameof(AppSettingsSnapshot.UseSystemChrome), StringComparer.OrdinalIgnoreCase);
if (languageChanged) if (languageChanged)
@@ -307,16 +310,33 @@ internal sealed class SettingsWindowService : ISettingsWindowService
? ThemeVariant.Dark ? ThemeVariant.Dark
: ThemeVariant.Light; : 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( var context = new ThemeColorContext(
accentColor, accentColor,
IsLightBackground: !themeState.IsNightMode, IsLightBackground: !themeState.IsNightMode,
IsLightNavBackground: !themeState.IsNightMode, IsLightNavBackground: !themeState.IsNightMode,
IsNightMode: themeState.IsNightMode); IsNightMode: themeState.IsNightMode,
MonetColors: monetPalette.MonetColors);
ThemeColorSystemService.ApplyThemeResources(window.Resources, context); ThemeColorSystemService.ApplyThemeResources(window.Resources, context);
GlassEffectService.ApplyGlassResources(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) private static Color TryParseThemeColor(string? colorText)
{ {
if (!string.IsNullOrWhiteSpace(colorText)) if (!string.IsNullOrWhiteSpace(colorText))

View File

@@ -1,4 +1,5 @@
using Avalonia.Controls; using System.Linq;
using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Media;
using LanMountainDesktop.Theme; using LanMountainDesktop.Theme;
@@ -67,7 +68,12 @@ public static class ThemeColorSystemService
private static AppThemePalette BuildPalette(ThemeColorContext context) 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 accentLight1 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.22);
var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38); var accentLight2 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.38);
var accentLight3 = ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.54); 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 accentDark3 = ColorMath.Blend(accent, Color.Parse("#FF020617"), 0.40);
var primary = context.IsNightMode ? accentLight1 : accentDark1; 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 surfaceBase = context.IsNightMode
var surfaceRaised = context.IsNightMode ? Color.Parse("#FF1E293B") : Color.Parse("#FFFFFFFF"); ? ColorMath.Blend(Color.Parse("#FF0A1018"), accent, 0.18)
var surfaceOverlay = context.IsNightMode ? Color.Parse("#CC0B1220") : Color.Parse("#CCE2E8F0"); : 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 textPrimaryPreferred = context.IsLightBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC");
var textPrimary = ColorMath.EnsureContrast(textPrimaryPreferred, surfaceRaised, WcagNormalTextContrast); 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("#FF0B1220"), 0.20), surfaceRaised, WcagNormalTextContrast)
: ColorMath.EnsureContrast(ColorMath.Blend(accent, Color.Parse("#FFFFFFFF"), 0.16), 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( var navText = ColorMath.EnsureContrast(
context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"), context.IsLightNavBackground ? Color.Parse("#FF0B1220") : Color.Parse("#FFF8FAFC"),
navSurface, navSurface,
@@ -104,16 +125,22 @@ public static class ThemeColorSystemService
var selectedSurfaceForContrast = ColorMath.Blend(accent, navSurface, 0.18); var selectedSurfaceForContrast = ColorMath.Blend(accent, navSurface, 0.18);
var navSelectedText = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), selectedSurfaceForContrast, WcagNormalTextContrast); 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 var navItemHoverBackground = context.IsLightNavBackground
? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, Color.Parse("#FFFFFFFF"), 0.48), 0x66) ? ColorMath.WithAlpha(ColorMath.Blend(accentLight2, surfaceRaised, 0.30), 0x7A)
: ColorMath.WithAlpha(ColorMath.Blend(accentDark1, Color.Parse("#33111827"), 0.32), 0x78); : ColorMath.WithAlpha(ColorMath.Blend(accentDark1, navSurface, 0.26), 0x88);
var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9); var navItemSelectedBackground = ColorMath.WithAlpha(accent, context.IsNightMode ? (byte)0xCE : (byte)0xD9);
var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast); var navSelectionIndicator = ColorMath.EnsureContrast(accentLight1, navSurface, WcagLargeTextContrast);
var toggleOn = context.IsNightMode ? accent : accentDark1; var toggleOn = context.IsNightMode ? accent : accentDark1;
var toggleOff = context.IsNightMode ? Color.Parse("#66475569") : Color.Parse("#66CBD5E1"); var toggleOff = context.IsNightMode
var toggleBorder = context.IsNightMode ? Color.Parse("#80E2E8F0") : Color.Parse("#8094A3B8"); ? 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); var onAccent = ColorMath.EnsureContrast(Color.Parse("#FFFFFFFF"), accent, WcagNormalTextContrast);
return new AppThemePalette( return new AppThemePalette(

View File

@@ -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);
}
}

View File

@@ -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<UpdateCheckResult> 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<UpdateDownloadResult> DownloadReleaseAsync(
UpdateCheckResult checkResult,
IProgress<double>? 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<char> 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]);
}
}

View File

@@ -1,4 +1,5 @@
using Avalonia.Media; using System.Collections.Generic;
using Avalonia.Media;
namespace LanMountainDesktop.Theme; namespace LanMountainDesktop.Theme;
@@ -6,4 +7,5 @@ public sealed record ThemeColorContext(
Color AccentColor, Color AccentColor,
bool IsLightBackground, bool IsLightBackground,
bool IsLightNavBackground, bool IsLightNavBackground,
bool IsNightMode); bool IsNightMode,
IReadOnlyList<Color>? MonetColors = null);

File diff suppressed because it is too large Load Diff

View File

@@ -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<SelectionOption> ClockFormats { get; }
public IReadOnlyList<SelectionOption> 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<SelectionOption> 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<SelectionOption> 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);
}

View File

@@ -198,11 +198,16 @@ public partial class MainWindow
private ThemeColorContext BuildAdaptiveThemeContext() 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( return new ThemeColorContext(
_selectedThemeColor, accentColor,
IsLightBackground: !_isNightMode, IsLightBackground: !_isNightMode,
IsLightNavBackground: !_isNightMode, IsLightNavBackground: !_isNightMode,
IsNightMode: _isNightMode); IsNightMode: _isNightMode,
MonetColors: palette.MonetColors);
} }
private void ApplyAdaptiveThemeResources() private void ApplyAdaptiveThemeResources()
@@ -386,7 +391,7 @@ public partial class MainWindow
return; return;
} }
var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath); var palette = _themeSettingsService.BuildPalette(enabled, _wallpaperPath, _selectedThemeColor.ToString());
_recommendedColors = palette.RecommendedColors; _recommendedColors = palette.RecommendedColors;
_monetColors = palette.MonetColors; _monetColors = palette.MonetColors;
} }
@@ -406,6 +411,32 @@ public partial class MainWindow
private void TriggerAutoUpdateCheckIfEnabled() 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() private void PersistSettings()
@@ -489,6 +520,7 @@ public partial class MainWindow
private AppSettingsSnapshot BuildAppSettingsSnapshot() private AppSettingsSnapshot BuildAppSettingsSnapshot()
{ {
var latestWeatherState = _weatherSettingsService.Get(); var latestWeatherState = _weatherSettingsService.Get();
var latestUpdateState = _updateSettingsService.Get();
return new AppSettingsSnapshot return new AppSettingsSnapshot
{ {
GridShortSideCells = _targetShortSideCells, GridShortSideCells = _targetShortSideCells,
@@ -513,6 +545,16 @@ public partial class MainWindow
WeatherNoTlsRequests = latestWeatherState.NoTlsRequests, WeatherNoTlsRequests = latestWeatherState.NoTlsRequests,
AutoStartWithWindows = _autoStartWithWindows, AutoStartWithWindows = _autoStartWithWindows,
AppRenderMode = _selectedAppRenderMode, 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], TopStatusComponentIds = [.. _topStatusComponentIds],
PinnedTaskbarActions = [.. _pinnedTaskbarActions.Select(v => v.ToString())], PinnedTaskbarActions = [.. _pinnedTaskbarActions.Select(v => v.ToString())],
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions, EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,

View File

@@ -6,96 +6,71 @@
xmlns:fi="using:FluentIcons.Avalonia.Fluent" xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage" x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
x:DataType="vm:AboutSettingsPageViewModel"> x:DataType="vm:AboutSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto"> <UserControl.Styles>
<StackPanel Classes="settings-page-container"> <Style Selector="StackPanel.about-page-container">
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Spacing" Value="0" />
<Setter Property="Margin" Value="0,12,0,24" />
</Style>
<!-- 应用信息分组 --> <Style Selector="Border.about-hero-card">
<controls:IconText Icon="Info" <Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
Text="{Binding AppInfoHeader}" <Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
Margin="0,0,0,4" /> <Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="24" />
<Setter Property="ClipToBounds" Value="True" />
<Setter Property="Margin" Value="0,0,0,18" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
</Style>
<ui:SettingsExpander Header="{Binding AppInfoHeader}"> <Style Selector="ui|InfoBar.about-static-info">
<ui:SettingsExpander.IconSource> <Setter Property="IsOpen" Value="True" />
<Setter Property="IsClosable" Value="False" />
<Setter Property="Severity" Value="Informational" />
<Setter Property="Margin" Value="0,0,0,12" />
</Style>
</UserControl.Styles>
<ScrollViewer HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Classes="about-page-container">
<Border x:Name="AboutHeroCard"
Classes="about-hero-card"
Height="240">
<Image Source="/Assets/about_banner.png"
Stretch="Uniform"
HorizontalAlignment="Center"
VerticalAlignment="Center" />
</Border>
<TextBlock Classes="settings-subsection-title"
Text="{Binding AppInfoHeader}" />
<ui:InfoBar Classes="about-static-info"
Title="{Binding VersionLabel}"
Message="{Binding VersionText}">
<ui:InfoBar.IconSource>
<fi:SymbolIconSource Symbol="Info" /> <fi:SymbolIconSource Symbol="Info" />
</ui:SettingsExpander.IconSource> </ui:InfoBar.IconSource>
<ui:SettingsExpanderItem> </ui:InfoBar>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding VersionLabel}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Opacity="0.82"
Text="{Binding VersionText}" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding RenderBackendLabel}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1"
HorizontalAlignment="Right"
Opacity="0.82"
Text="{Binding RenderBackendText}" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<Separator Classes="settings-separator" /> <ui:InfoBar Classes="about-static-info"
Title="{Binding CodenameLabel}"
<!-- 更新设置分组 --> Message="{Binding CodenameText}">
<controls:IconText Icon="ArrowSync" <ui:InfoBar.IconSource>
Text="{Binding UpdateHeader}" <fi:SymbolIconSource Symbol="Bookmark" />
Margin="0,0,0,4" /> </ui:InfoBar.IconSource>
</ui:InfoBar>
<ui:SettingsExpander Header="{Binding UpdateHeader}"
IsExpanded="True">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowSync" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding AutoCheckUpdatesLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding AutoCheckUpdates}" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding IncludePrereleaseUpdatesLabel}"
VerticalAlignment="Center" />
<ToggleSwitch Grid.Column="1"
IsChecked="{Binding IncludePrereleaseUpdates}" />
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding UpdateChannelLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="180"
ItemsSource="{Binding UpdateChannels}"
SelectedItem="{Binding SelectedUpdateChannel}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
<ui:SettingsExpanderItem>
<StackPanel Spacing="8">
<Button Command="{Binding CheckForUpdatesCommand}"
Content="{Binding CheckForUpdatesButtonText}" />
<TextBlock Opacity="0.76"
FontSize="12"
TextWrapping="Wrap"
Text="{Binding UpdateStatus}" />
</StackPanel>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<Border Classes="settings-section-card">
<StackPanel Spacing="12">
<controls:IconText Icon="WindowConsole"
Text="{Binding RenderBackendLabel}" />
<TextBlock Classes="settings-item-description"
Text="{Binding RenderBackendText}"
TextWrapping="Wrap" />
</StackPanel>
</Border>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -1,3 +1,6 @@
using System;
using Avalonia;
using Avalonia.Controls;
using LanMountainDesktop.PluginSdk; using LanMountainDesktop.PluginSdk;
using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Services.Settings;
using LanMountainDesktop.ViewModels; using LanMountainDesktop.ViewModels;
@@ -11,9 +14,12 @@ namespace LanMountainDesktop.Views.SettingsPages;
IconKey = "Info", IconKey = "Info",
SortOrder = 40, SortOrder = 40,
TitleLocalizationKey = "settings.about.title", TitleLocalizationKey = "settings.about.title",
DescriptionLocalizationKey = "settings.about.description")] DescriptionLocalizationKey = "settings.about.description",
HidePageTitle = true)]
public partial class AboutSettingsPage : SettingsPageBase public partial class AboutSettingsPage : SettingsPageBase
{ {
private const double HeroAspectRatio = 9d / 16d;
public AboutSettingsPage() public AboutSettingsPage()
: this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate())) : this(new AboutSettingsPageViewModel(HostSettingsFacadeProvider.GetOrCreate()))
{ {
@@ -24,7 +30,34 @@ public partial class AboutSettingsPage : SettingsPageBase
ViewModel = viewModel; ViewModel = viewModel;
DataContext = ViewModel; DataContext = ViewModel;
InitializeComponent(); InitializeComponent();
if (AboutHeroCard is not null)
{
AboutHeroCard.SizeChanged += OnAboutHeroCardSizeChanged;
UpdateHeroCardHeight(AboutHeroCard.Bounds.Width);
}
} }
public AboutSettingsPageViewModel ViewModel { get; } 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;
}
} }

View File

@@ -39,39 +39,6 @@
<ColorPicker Color="{Binding ThemeColorPickerValue}" /> <ColorPicker Color="{Binding ThemeColorPickerValue}" />
</ui:SettingsExpander.Footer> </ui:SettingsExpander.Footer>
</ui:SettingsExpander> </ui:SettingsExpander>
<Separator Classes="settings-separator" />
<controls:IconText Icon="Clock"
Text="{Binding ClockHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding ClockHeader}"
Description="{Binding ClockDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowClock}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*">
<TextBlock Text="{Binding ClockFormatLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="180"
ItemsSource="{Binding ClockFormats}"
SelectedItem="{Binding SelectedClockFormat}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</UserControl> </UserControl>

View File

@@ -0,0 +1,83 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.StatusBarSettingsPage"
x:DataType="vm:StatusBarSettingsPageViewModel">
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container">
<controls:IconText Icon="Apps"
Text="{Binding ComponentsHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding ClockHeader}"
Description="{Binding ClockDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Clock" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding ShowClock}" />
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem>
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding ClockFormatLabel}"
VerticalAlignment="Center" />
<ComboBox Grid.Column="1"
Width="220"
IsEnabled="{Binding ShowClock}"
ItemsSource="{Binding ClockFormats}"
SelectedItem="{Binding SelectedClockFormat}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
<Separator Classes="settings-separator" />
<controls:IconText Icon="Apps"
Text="{Binding SpacingHeader}"
Margin="0,0,0,4" />
<ui:SettingsExpander Header="{Binding SpacingHeader}"
Description="{Binding SpacingDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Apps" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="180"
ItemsSource="{Binding SpacingModes}"
SelectedItem="{Binding SelectedSpacingMode}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="vm:SelectionOption">
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
<ui:SettingsExpanderItem IsVisible="{Binding IsCustomSpacingVisible}">
<Grid ColumnDefinitions="Auto,*"
ColumnSpacing="16">
<TextBlock Text="{Binding CustomSpacingLabel}"
VerticalAlignment="Center" />
<ui:NumberBox Grid.Column="1"
Width="160"
Minimum="0"
Maximum="30"
SmallChange="1"
LargeChange="4"
SpinButtonPlacementMode="Inline"
Value="{Binding CustomSpacingPercent}" />
</Grid>
</ui:SettingsExpanderItem>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -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; }
}

View File

@@ -0,0 +1,224 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:LanMountainDesktop.ViewModels"
xmlns:controls="using:LanMountainDesktop.Controls"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
x:Class="LanMountainDesktop.Views.SettingsPages.UpdateSettingsPage"
x:DataType="vm:UpdateSettingsPageViewModel">
<UserControl.Styles>
<Style Selector="Border.update-status-card">
<Setter Property="Padding" Value="24" />
<Setter Property="Margin" Value="0,0,0,18" />
<Setter Property="CornerRadius" Value="24" />
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="BoxShadow" Value="0 6 18 #15000000" />
</Style>
<Style Selector="TextBlock.update-kv-label">
<Setter Property="FontSize" Value="12" />
<Setter Property="Opacity" Value="0.68" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
<Style Selector="TextBlock.update-kv-value">
<Setter Property="FontSize" Value="14" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="Foreground" Value="{DynamicResource AdaptiveTextPrimaryBrush}" />
</Style>
</UserControl.Styles>
<ScrollViewer VerticalScrollBarVisibility="Auto">
<StackPanel Classes="settings-page-container">
<TextBlock Classes="settings-section-title"
Text="{Binding PageTitle}" />
<TextBlock Classes="settings-section-description"
Text="{Binding PageDescription}" />
<Border Classes="update-status-card">
<StackPanel Spacing="18">
<Grid ColumnDefinitions="Auto,*,Auto"
ColumnSpacing="16">
<Border Classes="settings-section-card-icon-host"
Width="48"
Height="48">
<Viewbox Stretch="Uniform">
<fi:SymbolIcon Symbol="ArrowSync" />
</Viewbox>
</Border>
<StackPanel Grid.Column="1"
Spacing="4">
<TextBlock Classes="settings-card-header"
Margin="0"
Text="{Binding StatusCardTitle}" />
<TextBlock Classes="settings-item-description"
Text="{Binding StatusCardDescription}" />
</StackPanel>
<Button Grid.Column="2"
Classes="settings-accent-button"
Command="{Binding CheckForUpdatesCommand}"
Content="{Binding CheckForUpdatesButtonText}" />
</Grid>
<Grid ColumnDefinitions="*,*"
ColumnSpacing="14"
RowSpacing="12">
<StackPanel Grid.Column="0"
Spacing="4">
<TextBlock Classes="update-kv-label"
Text="{Binding CurrentVersionLabel}" />
<TextBlock Classes="update-kv-value"
Text="{Binding CurrentVersionText}" />
</StackPanel>
<StackPanel Grid.Column="1"
Spacing="4"
IsVisible="{Binding IsLatestVersionVisible}">
<TextBlock Classes="update-kv-label"
Text="{Binding LatestVersionLabel}" />
<TextBlock Classes="update-kv-value"
Text="{Binding LatestVersionText}" />
</StackPanel>
<StackPanel Grid.Row="1"
Grid.Column="0"
Spacing="4"
IsVisible="{Binding IsPublishedAtVisible}">
<TextBlock Classes="update-kv-label"
Text="{Binding PublishedAtLabel}" />
<TextBlock Classes="update-kv-value"
Text="{Binding PublishedAtText}" />
</StackPanel>
<StackPanel Grid.Row="1"
Grid.Column="1"
Spacing="4"
IsVisible="{Binding IsLastCheckedVisible}">
<TextBlock Classes="update-kv-label"
Text="{Binding LastCheckedLabel}" />
<TextBlock Classes="update-kv-value"
Text="{Binding LastCheckedText}" />
</StackPanel>
</Grid>
<StackPanel Spacing="8">
<TextBlock Classes="settings-item-description"
Text="{Binding UpdateStatus}" />
<ProgressBar Minimum="0"
Maximum="100"
Value="{Binding DownloadProgressValue}"
IsVisible="{Binding IsDownloadProgressVisible}" />
<TextBlock Classes="settings-item-description"
IsVisible="{Binding IsDownloadProgressVisible}"
Text="{Binding DownloadProgressText}" />
</StackPanel>
<StackPanel Orientation="Horizontal"
Spacing="10">
<Button Command="{Binding DownloadLatestReleaseCommand}"
Content="{Binding DownloadButtonText}"
IsVisible="{Binding IsDownloadButtonVisible}" />
<Button Classes="settings-accent-button"
Command="{Binding InstallPendingUpdateCommand}"
Content="{Binding InstallNowButtonText}"
IsVisible="{Binding IsInstallButtonVisible}" />
</StackPanel>
</StackPanel>
</Border>
<TextBlock Classes="settings-subsection-title"
Text="{Binding PreferencesHeader}" />
<TextBlock Classes="settings-section-description"
Margin="0,0,0,18"
Text="{Binding PreferencesDescription}" />
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding UpdateChannelLabel}"
Description="{Binding SelectedUpdateChannelDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="BranchFork" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="220"
ItemsSource="{Binding UpdateChannelOptions}"
SelectedItem="{Binding SelectedUpdateChannelOption}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding UpdateSourceLabel}"
Description="{Binding SelectedUpdateSourceDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="GlobeArrowForward" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="220"
ItemsSource="{Binding UpdateSourceOptions}"
SelectedItem="{Binding SelectedUpdateSourceOption}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding UpdateModeLabel}"
Description="{Binding SelectedUpdateModeDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="Options" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ComboBox Width="260"
ItemsSource="{Binding UpdateModeOptions}"
SelectedItem="{Binding SelectedUpdateModeOption}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Label}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding DownloadThreadsLabel}"
Description="{Binding DownloadThreadsDescription}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ArrowDownload" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ui:NumberBox Width="160"
Minimum="1"
Maximum="128"
SpinButtonPlacementMode="Inline"
Value="{Binding DownloadThreadsSliderValue}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
<ui:SettingsExpander Classes="settings-expander-card"
Header="{Binding AutoCheckUpdatesLabel}">
<ui:SettingsExpander.IconSource>
<fi:SymbolIconSource Symbol="ClockAlarm" />
</ui:SettingsExpander.IconSource>
<ui:SettingsExpander.Footer>
<ToggleSwitch IsChecked="{Binding AutoCheckUpdates}" />
</ui:SettingsExpander.Footer>
</ui:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@@ -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; }
}

View File

@@ -216,8 +216,11 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext
ViewModel.CurrentPageTitle = descriptor.Title; ViewModel.CurrentPageTitle = descriptor.Title;
ViewModel.CurrentPageDescription = descriptor.Description; ViewModel.CurrentPageDescription = descriptor.Description;
ViewModel.CurrentPageId = descriptor.PageId; ViewModel.CurrentPageId = descriptor.PageId;
ViewModel.IsPageTitleVisible = !descriptor.HidePageTitle;
TrySelectNavigationItem(descriptor.PageId); TrySelectNavigationItem(descriptor.PageId);
SyncTitleText(); SyncTitleText();
UpdateResponsiveLayout();
RequestResponsiveLayoutRefresh();
} }
private SettingsPageDescriptor? ResolveDescriptor(string? pageId) private SettingsPageDescriptor? ResolveDescriptor(string? pageId)