mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
settings_re10
This commit is contained in:
75
.github/workflows/release.yml
vendored
75
.github/workflows/release.yml
vendored
@@ -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>.*?</Version>', "<Version>$VERSION</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>$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>$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: |
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
BIN
LanMountainDesktop/Assets/about_banner.png
Normal file
BIN
LanMountainDesktop/Assets/about_banner.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 999 KiB |
@@ -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",
|
||||
|
||||
@@ -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": "需要重启应用",
|
||||
|
||||
@@ -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<string> TopStatusComponentIds { get; set; } = [];
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -172,6 +172,8 @@ public sealed class GitHubReleaseUpdateService : IDisposable
|
||||
public async Task<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
string downloadSource,
|
||||
int maxParallelSegments,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
@@ -193,11 +195,14 @@ public sealed class GitHubReleaseUpdateService : IDisposable
|
||||
var progressAdapter = progress is null
|
||||
? null
|
||||
: new Progress<DownloadProgressInfo>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 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 neutralBase = context.IsNightMode ? Color.Parse("#FF202020") : Color.Parse("#FFF3F3F3");
|
||||
var neutralElevated = context.IsNightMode ? Color.Parse("#FF2C2C2C") : Color.Parse("#FFFAFAFA");
|
||||
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);
|
||||
|
||||
// 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 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<string> 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<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
string downloadSource,
|
||||
int maxParallelSegments,
|
||||
IProgress<double>? progress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
}
|
||||
@@ -197,6 +209,7 @@ public interface IPluginMarketSettingsService
|
||||
public interface IApplicationInfoService
|
||||
{
|
||||
string GetAppVersionText();
|
||||
string GetAppCodenameText();
|
||||
AppRenderBackendInfo GetRenderBackendInfo();
|
||||
}
|
||||
|
||||
|
||||
@@ -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<UpdateDownloadResult> DownloadAssetAsync(
|
||||
GitHubReleaseAsset asset,
|
||||
string destinationFilePath,
|
||||
string downloadSource,
|
||||
int maxParallelSegments,
|
||||
IProgress<double>? 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<AssemblyInformationalVersionAttribute>()?
|
||||
.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()
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(
|
||||
|
||||
63
LanMountainDesktop/Services/UpdateSettingsValues.cs
Normal file
63
LanMountainDesktop/Services/UpdateSettingsValues.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
291
LanMountainDesktop/Services/UpdateWorkflowService.cs
Normal file
291
LanMountainDesktop/Services/UpdateWorkflowService.cs
Normal 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]);
|
||||
}
|
||||
}
|
||||
@@ -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<Color>? MonetColors = null);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
214
LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs
Normal file
214
LanMountainDesktop/ViewModels/StatusBarSettingsPageViewModel.cs
Normal 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);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -6,96 +6,71 @@
|
||||
xmlns:fi="using:FluentIcons.Avalonia.Fluent"
|
||||
x:Class="LanMountainDesktop.Views.SettingsPages.AboutSettingsPage"
|
||||
x:DataType="vm:AboutSettingsPageViewModel">
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Classes="settings-page-container">
|
||||
<UserControl.Styles>
|
||||
<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>
|
||||
|
||||
<!-- 应用信息分组 -->
|
||||
<controls:IconText Icon="Info"
|
||||
Text="{Binding AppInfoHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
<Style Selector="Border.about-hero-card">
|
||||
<Setter Property="Background" Value="{DynamicResource AdaptiveSurfaceRaisedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource AdaptiveGlassPanelBorderBrush}" />
|
||||
<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}">
|
||||
<ui:SettingsExpander.IconSource>
|
||||
<Style Selector="ui|InfoBar.about-static-info">
|
||||
<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" />
|
||||
</ui:SettingsExpander.IconSource>
|
||||
<ui:SettingsExpanderItem>
|
||||
<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>
|
||||
</ui:InfoBar.IconSource>
|
||||
</ui:InfoBar>
|
||||
|
||||
<Separator Classes="settings-separator" />
|
||||
|
||||
<!-- 更新设置分组 -->
|
||||
<controls:IconText Icon="ArrowSync"
|
||||
Text="{Binding UpdateHeader}"
|
||||
Margin="0,0,0,4" />
|
||||
|
||||
<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>
|
||||
<ui:InfoBar Classes="about-static-info"
|
||||
Title="{Binding CodenameLabel}"
|
||||
Message="{Binding CodenameText}">
|
||||
<ui:InfoBar.IconSource>
|
||||
<fi:SymbolIconSource Symbol="Bookmark" />
|
||||
</ui:InfoBar.IconSource>
|
||||
</ui:InfoBar>
|
||||
|
||||
<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>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,39 +39,6 @@
|
||||
<ColorPicker Color="{Binding ThemeColorPickerValue}" />
|
||||
</ui:SettingsExpander.Footer>
|
||||
</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>
|
||||
</ScrollViewer>
|
||||
</UserControl>
|
||||
|
||||
@@ -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>
|
||||
@@ -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; }
|
||||
}
|
||||
224
LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
Normal file
224
LanMountainDesktop/Views/SettingsPages/UpdateSettingsPage.axaml
Normal 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>
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user