mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 00:54:26 +08:00
settings_re10
This commit is contained in:
@@ -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 neutralBase = context.IsNightMode ? Color.Parse("#FF202020") : Color.Parse("#FFF3F3F3");
|
||||
var neutralElevated = context.IsNightMode ? Color.Parse("#FF2C2C2C") : Color.Parse("#FFFAFAFA");
|
||||
|
||||
// Mica 效果:将壁纸颜色混合到中性基色中
|
||||
var micaBackground = ColorMath.Blend(neutralBase, context.AccentColor, wallpaperTintOpacity);
|
||||
var micaElevated = ColorMath.Blend(neutralElevated, context.AccentColor, wallpaperTintOpacity * 0.8);
|
||||
|
||||
// 按钮颜色
|
||||
var buttonBackground = context.IsNightMode ?
|
||||
Color.FromArgb(0x33, micaBackground.R, micaBackground.G, micaBackground.B) :
|
||||
Color.FromArgb(0x4D, micaBackground.R, micaBackground.G, micaBackground.B);
|
||||
|
||||
var monetColors = context.MonetColors?.Where(color => color.A > 0).ToArray() ?? [];
|
||||
var primary = monetColors.Length > 0 ? monetColors[0] : context.AccentColor;
|
||||
var secondary = monetColors.Length > 1
|
||||
? monetColors[1]
|
||||
: ColorMath.Blend(primary, Color.Parse("#FFFFFFFF"), 0.12);
|
||||
|
||||
var panelBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF101722"), primary, 0.26)
|
||||
: ColorMath.Blend(Color.Parse("#FFF9FBFE"), primary, 0.14);
|
||||
var panelRaised = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF15202C"), secondary, 0.30)
|
||||
: ColorMath.Blend(Color.Parse("#FFFFFFFF"), secondary, 0.18);
|
||||
var overlayBase = context.IsNightMode
|
||||
? ColorMath.Blend(Color.Parse("#FF0E1622"), primary, 0.36)
|
||||
: ColorMath.Blend(Color.Parse("#FFF3F7FD"), primary, 0.20);
|
||||
|
||||
var buttonBackground = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x4D : (byte)0x52,
|
||||
panelRaised.R,
|
||||
panelRaised.G,
|
||||
panelRaised.B);
|
||||
var buttonBorder = Color.FromArgb(
|
||||
context.IsNightMode ? (byte)0x36 : (byte)0x26,
|
||||
primary.R,
|
||||
primary.G,
|
||||
primary.B);
|
||||
|
||||
resources["AdaptiveButtonBackgroundBrush"] = new SolidColorBrush(buttonBackground);
|
||||
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(0x1A, neutralElevated.R, neutralElevated.G, neutralElevated.B));
|
||||
resources["AdaptiveButtonBorderBrush"] = new SolidColorBrush(buttonBorder);
|
||||
resources["AdaptiveButtonHoverBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(buttonBackground, context.IsNightMode ? (byte)0x4D : (byte)0x66));
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.18), context.IsNightMode ? (byte)0x72 : (byte)0x7A));
|
||||
resources["AdaptiveButtonPressedBackgroundBrush"] = new SolidColorBrush(
|
||||
ColorMath.WithAlpha(buttonBackground, context.IsNightMode ? (byte)0x66 : (byte)0x80));
|
||||
ColorMath.WithAlpha(ColorMath.Blend(buttonBackground, primary, 0.30), context.IsNightMode ? (byte)0x8A : (byte)0x8C));
|
||||
|
||||
// 面板颜色 - 使用 Mica 材质
|
||||
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF0 : (byte)0xF8,
|
||||
micaBackground.R, micaBackground.G, micaBackground.B));
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF2 : (byte)0xFA, panelBase.R, panelBase.G, panelBase.B));
|
||||
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(0x1F, neutralElevated.R, neutralElevated.G, neutralElevated.B));
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x38 : (byte)0x24, primary.R, primary.G, primary.B));
|
||||
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF4 : (byte)0xFB,
|
||||
micaElevated.R, micaElevated.G, micaElevated.B));
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xF6 : (byte)0xFC, panelRaised.R, panelRaised.G, panelRaised.B));
|
||||
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(0x29, neutralElevated.R, neutralElevated.G, neutralElevated.B));
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0x4A : (byte)0x2C, secondary.R, secondary.G, secondary.B));
|
||||
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF2,
|
||||
micaBackground.R, micaBackground.G, micaBackground.B));
|
||||
Color.FromArgb(context.IsNightMode ? (byte)0xEA : (byte)0xF4, overlayBase.R, overlayBase.G, overlayBase.B));
|
||||
|
||||
// 模糊半径(Mica 不需要强模糊)
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 20.0 : 30.0;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 25.0 : 35.0;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 30.0 : 40.0;
|
||||
|
||||
// 不透明度(Mica 材质接近不透明)
|
||||
resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.99 : 1.0;
|
||||
resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 1.0 : 1.0;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.94 : 0.97;
|
||||
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.01 : 0.008;
|
||||
resources["AdaptiveGlassPanelBlurRadius"] = context.IsNightMode ? 22.0 : 28.0;
|
||||
resources["AdaptiveGlassStrongBlurRadius"] = context.IsNightMode ? 28.0 : 34.0;
|
||||
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 34.0 : 40.0;
|
||||
resources["AdaptiveGlassPanelOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassStrongOpacity"] = 1.0;
|
||||
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.95 : 0.98;
|
||||
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.012 : 0.008;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user