From 557b79e8c0f4ca7447e57a7cc3988f3f8628a1f3 Mon Sep 17 00:00:00 2001 From: lincube Date: Sun, 15 Mar 2026 21:27:48 +0800 Subject: [PATCH] =?UTF-8?q?=EF=BC=8C0.6.0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构了设置系统。解决了大量的bug,正式添加了图标。引入了遥测的同意与许可(暂无实际功能) --- LanMountainDesktop/App.axaml.cs | 4 +- LanMountainDesktop/Assets/logo_nightly.ico | Bin 0 -> 11226 bytes LanMountainDesktop/Assets/logo_nightly.png | Bin 0 -> 13761 bytes LanMountainDesktop/Assets/logo_nightly.svg | 38 + .../Assets/logo_nightly_render.html | 31 + LanMountainDesktop/LanMountainDesktop.csproj | 1 + LanMountainDesktop/Services/AppLogoService.cs | 72 ++ .../Services/AppearanceThemeService.cs | 3 +- .../Services/CurrentUserProfileService.cs | 196 +++++ .../Services/WallpaperImageBrushFactory.cs | 69 ++ .../WallpaperSettingsPageViewModel.cs | 61 +- .../Views/MainWindow.ComponentSystem.cs | 226 ++++-- .../Views/MainWindow.SettingsHardCut.Stubs.cs | 295 +++++-- LanMountainDesktop/Views/MainWindow.axaml | 232 ++++-- LanMountainDesktop/Views/MainWindow.axaml.cs | 742 ++---------------- .../SettingsPages/WallpaperSettingsPage.axaml | 31 +- LanMountainDesktop/Views/SettingsWindow.axaml | 1 - .../Views/SettingsWindow.axaml.cs | 2 + .../installer/LanMountainDesktop.iss | 8 +- 19 files changed, 1143 insertions(+), 869 deletions(-) create mode 100644 LanMountainDesktop/Assets/logo_nightly.ico create mode 100644 LanMountainDesktop/Assets/logo_nightly.png create mode 100644 LanMountainDesktop/Assets/logo_nightly.svg create mode 100644 LanMountainDesktop/Assets/logo_nightly_render.html create mode 100644 LanMountainDesktop/Services/AppLogoService.cs create mode 100644 LanMountainDesktop/Services/CurrentUserProfileService.cs create mode 100644 LanMountainDesktop/Services/WallpaperImageBrushFactory.cs diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 0c20758..908d895 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -44,6 +44,7 @@ public partial class App : Application private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate(); + private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate(); private readonly LocalizationService _localizationService = new(); private readonly IHostApplicationLifecycle _hostApplicationLifecycle = new HostApplicationLifecycleService(); private readonly IDetachedComponentLibraryWindowService _detachedComponentLibraryWindowService = new DetachedComponentLibraryWindowService(); @@ -229,10 +230,9 @@ public partial class App : Application { DisposeTrayIcon(); - using var iconStream = AssetLoader.Open(new Uri("avares://LanMountainDesktop/Assets/avalonia-logo.ico")); var trayIcon = new TrayIcon { - Icon = new WindowIcon(iconStream), + Icon = _appLogoService.CreateTrayIcon(), ToolTipText = L("tray.tooltip", "LanMountainDesktop"), Menu = BuildTrayMenu(), IsVisible = true diff --git a/LanMountainDesktop/Assets/logo_nightly.ico b/LanMountainDesktop/Assets/logo_nightly.ico new file mode 100644 index 0000000000000000000000000000000000000000..aa1524dc89687495338fe35fa0ce52a9c40000ef GIT binary patch literal 11226 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

2q4U{KjPr=;`qbvSP6xQ?K}7zGv2~U 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%0Dq|X{?6Mk~i?8NL81_tZK{l z=ZfGi-FPwHDUne%dKx~B7q5^uD92sSqcHSS>FVLX4N+U!Q$@AVG}EpiP((bS3P(lP z>Fe!D=u~mF+ckx1xPK^MU$z+?axz5rz`8Q4N>zL4SP z|9P~ga7u%Yw$|dyvm>_9k9Fd(+Tf|{2(utD{{AZbH7R+2W<=QU7^v))A7Z3GaD1%v zQ~|UDki>*ciZT)fQx`6JUV$(IaosrAUpyqO9Q~$~le*ej`%~$lF^tjOet7pjHo74} z#ZISlxUvc^wv50i=n`lSVc{FJZmlHXnoB+aE5mHvg7lq<^q8Q94jOtckGUi|KB?Gx zMzMs(6t}|OJ^`_- zn`&?NZd|FMo*Hf1edww|+>ziKnlN2r*N~J)#W}6yxuHtqJAnpDQ!Jbqg z&~t2ycsluDv}~uFC~*@ZjEstcT>TbWcQEH!N)HS0 zd${@)hCMuhT1`xCqw|OCb*#_(4Y12Al@I3mfu^CAlx^a_Xm)HoCDyq17Fej1odkSZ z;zFISE-0)d$m$+gy#Sd$4(x)WVI}eSFOB7MMa`S3Jp3`NPiX2qk#<8UXYV=3r#J?U zK~V_#eQ`pjB}L}sH(~XsA{JelSTD2^FTZVQ7fN~B^%vL_^!a?DCoDMEK=4)Y4dtgu zo@|0!3rsN3&1808gF(wAO$w!%D3x!P!zT zP-oc65B&viO{T-G*vXetPMwJ$EM9aTQP$Ph(FQtLP`z#{?G}G5Lu)hvHOt+XtXH*AiMuaL6LDD@5nOpIdOpB}KNe5x z-tjUQDXOR*OA+N87Y?bQhea! zz0b$}{Xu`f<+j1eFTT`MWW)*cj!j0tqYYSr;qxJAga|$VldNGUl>E#s{mC9!{z|T4 z8?x}o?&A1s?jCiV*uhb$yyE=VYAZb;c5(9ahn%V*W%1wLpe5Ux0jpu z@A9*DPs4*bngPUAABvi!2t1lRsmwH^+&_5r!(S0#XlA&J1oNZ}}cA5cUchRtf+O zraRsS%2`~CtA76`g#S|XJ(RO0v^)Lf?VjYlTbhBpY%O^uBCp2JSh)?=)OB zfoG0>ZK8c#cD4kZK7#q|^>pN{F1OhPBlvb0+)!{@GGRkvyOGd00|Ls5ANR!J1p7x+vnARDjg{DTi+G}laJx2Xqi|Ec!!Z@bX%(RdZ*$MxfdKk8aCjT2Q>qCdr#tdzB1S z=n1Y(_cQjSu#3fi$;gu12R>7OlI**P*44+*;2H1jaV=f_fOPlGl;eYl=@XSEqG0;| z)+1rds=j&MEbi0o@+;5f$*bMU{CE9*pfw=_X`*v^U$eKE=vUOK;t%EZ46wrpk}5!i zhmv=B-S{a(i&J^QMDSp>s7kjI`=~$^xWqh_00y#mVPT2}jv89vg`nNmB%R+=lZL_$ z6;-4XSegNqX)HGvPHzk84oCCE37fEn9QHktM{AOFu$f=X3?x-1q&$R@Q~(`-jt3E> z3Rv-|x`5}I#_Du<+l{X5C@5Sw>j^?dLl|~xyjoEoX}h4rwH@jT|Kcu|>PEjx#q~HCGr*5cgi+5bDs_LV9bxU*9ZuzP+)xi|G5v z0Qs{Jc4cf=x6BbDQF-#fG~LytTkj$!J$^oF9 zhus-k))X$~=SWxH|Ngw>e}6yo|Ld=bs@Axdl^#FN#tQGdr40SND6Px7Y6Ysd9{(TX Ci+>dW literal 0 HcmV?d00001 diff --git a/LanMountainDesktop/Assets/logo_nightly.svg b/LanMountainDesktop/Assets/logo_nightly.svg new file mode 100644 index 0000000..ccf7d2e --- /dev/null +++ b/LanMountainDesktop/Assets/logo_nightly.svg @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Assets/logo_nightly_render.html b/LanMountainDesktop/Assets/logo_nightly_render.html new file mode 100644 index 0000000..1824636 --- /dev/null +++ b/LanMountainDesktop/Assets/logo_nightly_render.html @@ -0,0 +1,31 @@ + + + + + + + +

+ logo +
+ + diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 522d396..791fdb7 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -5,6 +5,7 @@ enable 1.0.0 app.manifest + Assets\logo_nightly.ico true true diff --git a/LanMountainDesktop/Services/AppLogoService.cs b/LanMountainDesktop/Services/AppLogoService.cs new file mode 100644 index 0000000..96c2d43 --- /dev/null +++ b/LanMountainDesktop/Services/AppLogoService.cs @@ -0,0 +1,72 @@ +using System; +using Avalonia.Controls; +using Avalonia.Platform; + +namespace LanMountainDesktop.Services; + +public enum AppLogoVariant +{ + Auto = 0, + Day = 1, + Night = 2 +} + +public interface IAppLogoService +{ + WindowIcon CreateWindowIcon(AppLogoVariant variant = AppLogoVariant.Auto); + WindowIcon CreateTrayIcon(AppLogoVariant variant = AppLogoVariant.Auto); + Uri GetVectorLogoUri(AppLogoVariant variant = AppLogoVariant.Auto); +} + +internal sealed class AppLogoService : IAppLogoService +{ + private static readonly Uri NightVectorLogoUri = new("avares://LanMountainDesktop/Assets/logo_nightly.svg"); + private static readonly Uri DayVectorLogoUri = new("avares://LanMountainDesktop/Assets/logo_nightly.svg"); + private static readonly Uri NightIconUri = new("avares://LanMountainDesktop/Assets/logo_nightly.ico"); + private static readonly Uri DayIconUri = new("avares://LanMountainDesktop/Assets/logo_nightly.ico"); + + public WindowIcon CreateWindowIcon(AppLogoVariant variant = AppLogoVariant.Auto) => CreateIcon(ResolveIconUri(variant)); + + public WindowIcon CreateTrayIcon(AppLogoVariant variant = AppLogoVariant.Auto) => CreateIcon(ResolveIconUri(variant)); + + public Uri GetVectorLogoUri(AppLogoVariant variant = AppLogoVariant.Auto) => ResolveVectorLogoUri(variant); + + private static WindowIcon CreateIcon(Uri assetUri) + { + using var stream = AssetLoader.Open(assetUri); + return new WindowIcon(stream); + } + + private static Uri ResolveIconUri(AppLogoVariant variant) => ResolveVariant(variant) switch + { + AppLogoVariant.Day => DayIconUri, + _ => NightIconUri + }; + + private static Uri ResolveVectorLogoUri(AppLogoVariant variant) => ResolveVariant(variant) switch + { + AppLogoVariant.Day => DayVectorLogoUri, + _ => NightVectorLogoUri + }; + + private static AppLogoVariant ResolveVariant(AppLogoVariant variant) => variant switch + { + AppLogoVariant.Day => AppLogoVariant.Day, + AppLogoVariant.Night => AppLogoVariant.Night, + _ => AppLogoVariant.Night + }; +} + +internal static class HostAppLogoProvider +{ + private static readonly object Gate = new(); + private static IAppLogoService? _instance; + + public static IAppLogoService GetOrCreate() + { + lock (Gate) + { + return _instance ??= new AppLogoService(); + } + } +} diff --git a/LanMountainDesktop/Services/AppearanceThemeService.cs b/LanMountainDesktop/Services/AppearanceThemeService.cs index d90a3a3..c3a6292 100644 --- a/LanMountainDesktop/Services/AppearanceThemeService.cs +++ b/LanMountainDesktop/Services/AppearanceThemeService.cs @@ -464,6 +464,7 @@ internal sealed class MaterialSurfaceService : IMaterialSurfaceService internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposable { private static readonly Color DefaultAccentColor = Color.Parse("#FF3B82F6"); + private static readonly Color NeutralFallbackSeedColor = Color.Parse("#FF8A8A8A"); private readonly ISettingsFacadeService _settingsFacade; private readonly ISystemWallpaperService _systemWallpaperService; private readonly IWindowMaterialService _windowMaterialService; @@ -811,7 +812,7 @@ internal sealed class AppearanceThemeService : IAppearanceThemeService, IDisposa private WallpaperPaletteResolution BuildFallbackWallpaperPaletteResolution(bool nightMode, string? resolvedWallpaperPath) { - var palette = _settingsFacade.Theme.BuildPalette(nightMode, null, null); + var palette = _monetColorService.BuildPaletteFromSeedCandidates([], nightMode, NeutralFallbackSeedColor); return new WallpaperPaletteResolution( palette, [], diff --git a/LanMountainDesktop/Services/CurrentUserProfileService.cs b/LanMountainDesktop/Services/CurrentUserProfileService.cs new file mode 100644 index 0000000..4f485b0 --- /dev/null +++ b/LanMountainDesktop/Services/CurrentUserProfileService.cs @@ -0,0 +1,196 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Avalonia.Media.Imaging; + +namespace LanMountainDesktop.Services; + +public sealed record CurrentUserProfileSnapshot( + string DisplayName, + Bitmap? AvatarBitmap, + string FallbackMonogram, + bool IsPlaceholder); + +public interface ICurrentUserProfileService +{ + CurrentUserProfileSnapshot GetCurrentProfile(); +} + +internal sealed class CurrentUserProfileService : ICurrentUserProfileService, IDisposable +{ + private readonly object _gate = new(); + private CurrentUserProfileSnapshot? _cachedSnapshot; + private Bitmap? _cachedAvatarBitmap; + + public CurrentUserProfileSnapshot GetCurrentProfile() + { + lock (_gate) + { + if (_cachedSnapshot is not null) + { + return _cachedSnapshot; + } + + var displayName = ResolveDisplayName(); + _cachedAvatarBitmap = TryLoadSystemAvatarBitmap(); + _cachedSnapshot = new CurrentUserProfileSnapshot( + displayName, + _cachedAvatarBitmap, + BuildMonogram(displayName), + _cachedAvatarBitmap is null); + return _cachedSnapshot; + } + } + + public void Dispose() + { + lock (_gate) + { + _cachedSnapshot = null; + _cachedAvatarBitmap?.Dispose(); + _cachedAvatarBitmap = null; + } + } + + private static string ResolveDisplayName() + { + var userName = Environment.UserName?.Trim(); + return string.IsNullOrWhiteSpace(userName) ? "User" : userName; + } + + private static Bitmap? TryLoadSystemAvatarBitmap() + { + foreach (var path in EnumerateAvatarCandidates()) + { + try + { + using var stream = File.OpenRead(path); + return new Bitmap(stream); + } + catch + { + // Ignore unreadable avatar files and continue with the next candidate. + } + } + + return null; + } + + private static IEnumerable EnumerateAvatarCandidates() + { + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + + foreach (var path in EnumerateDirectoryCandidates( + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + "Microsoft", + "Windows", + "AccountPictures"))) + { + if (seen.Add(path)) + { + yield return path; + } + } + + foreach (var path in EnumerateDirectoryCandidates( + Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), + "Microsoft", + "Windows", + "AccountPictures"))) + { + if (seen.Add(path)) + { + yield return path; + } + } + + var commonPicturesDirectory = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData), + "Microsoft", + "User Account Pictures"); + + foreach (var fileName in new[] + { + "user-448.png", + "user-240.png", + "user-192.png", + "user-96.png", + "user-64.png", + "user-48.png", + "user.png" + }) + { + var path = Path.Combine(commonPicturesDirectory, fileName); + if (File.Exists(path) && seen.Add(path)) + { + yield return path; + } + } + } + + private static IEnumerable EnumerateDirectoryCandidates(string directoryPath) + { + if (!Directory.Exists(directoryPath)) + { + yield break; + } + + var files = Directory.EnumerateFiles(directoryPath) + .Where(path => + { + var extension = Path.GetExtension(path); + return extension.Equals(".png", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".jpg", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".jpeg", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".bmp", StringComparison.OrdinalIgnoreCase) || + extension.Equals(".webp", StringComparison.OrdinalIgnoreCase); + }) + .Select(path => new FileInfo(path)) + .OrderByDescending(file => file.LastWriteTimeUtc) + .ThenByDescending(file => file.Length); + + foreach (var file in files) + { + yield return file.FullName; + } + } + + private static string BuildMonogram(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return "?"; + } + + var letters = text + .Trim() + .Split(' ', StringSplitOptions.RemoveEmptyEntries) + .Select(part => part[0]) + .Take(2) + .ToArray(); + + if (letters.Length == 0) + { + return "?"; + } + + return new string(letters).ToUpperInvariant(); + } +} + +internal static class HostCurrentUserProfileProvider +{ + private static readonly object Gate = new(); + private static ICurrentUserProfileService? _instance; + + public static ICurrentUserProfileService GetOrCreate() + { + lock (Gate) + { + return _instance ??= new CurrentUserProfileService(); + } + } +} diff --git a/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs b/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs new file mode 100644 index 0000000..aba8813 --- /dev/null +++ b/LanMountainDesktop/Services/WallpaperImageBrushFactory.cs @@ -0,0 +1,69 @@ +using System; +using Avalonia; +using Avalonia.Media; +using Avalonia.Media.Imaging; + +namespace LanMountainDesktop.Services; + +internal static class WallpaperImageBrushFactory +{ + internal const string Fill = "Fill"; + internal const string Fit = "Fit"; + internal const string StretchMode = "Stretch"; + internal const string Center = "Center"; + internal const string Tile = "Tile"; + + public static string NormalizePlacement(string? placement) + { + return placement switch + { + _ when string.Equals(placement, Fit, StringComparison.OrdinalIgnoreCase) => Fit, + _ when string.Equals(placement, StretchMode, StringComparison.OrdinalIgnoreCase) => StretchMode, + _ when string.Equals(placement, Center, StringComparison.OrdinalIgnoreCase) => Center, + _ when string.Equals(placement, Tile, StringComparison.OrdinalIgnoreCase) => Tile, + _ => Fill + }; + } + + public static ImageBrush Create(Bitmap bitmap, string? placement) + { + var normalizedPlacement = NormalizePlacement(placement); + var brush = new ImageBrush(bitmap) + { + AlignmentX = AlignmentX.Center, + AlignmentY = AlignmentY.Center, + Stretch = Stretch.UniformToFill, + TileMode = TileMode.None + }; + + switch (normalizedPlacement) + { + case Fit: + brush.Stretch = Stretch.Uniform; + break; + + case StretchMode: + brush.Stretch = Stretch.Fill; + break; + + case Center: + brush.Stretch = Stretch.None; + break; + + case Tile: + brush.AlignmentX = AlignmentX.Left; + brush.AlignmentY = AlignmentY.Top; + brush.Stretch = Stretch.None; + brush.TileMode = TileMode.Tile; + brush.DestinationRect = new RelativeRect( + 0, + 0, + Math.Max(1, bitmap.Size.Width), + Math.Max(1, bitmap.Size.Height), + RelativeUnit.Absolute); + break; + } + + return brush; + } +} diff --git a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs index 5c5db21..5b351f5 100644 --- a/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs +++ b/LanMountainDesktop/ViewModels/WallpaperSettingsPageViewModel.cs @@ -79,9 +79,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase [ObservableProperty] private bool _isSolidColor; + [ObservableProperty] + private bool _isImage; + + [ObservableProperty] + private bool _isVideo; + [ObservableProperty] private Bitmap? _previewImage; + [ObservableProperty] + private IBrush? _previewBrush; + + [ObservableProperty] + private string _videoModeHintText = string.Empty; + public void Load() { var wallpaper = _settingsFacade.Wallpaper.Get(); @@ -98,18 +110,21 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase ?? WallpaperPlacements[0]; UpdateVisibility(); - UpdatePreviewImage(WallpaperPath); + UpdatePreviewFromCurrentSelection(); } partial void OnSelectedWallpaperTypeChanged(SelectionOption value) { UpdateVisibility(); + UpdatePreviewFromCurrentSelection(); if (_isInitializing) return; SaveWallpaper(); } private void UpdateVisibility() { + IsImage = SelectedWallpaperType?.Value == "Image"; + IsVideo = SelectedWallpaperType?.Value == "Video"; IsImageOrVideo = SelectedWallpaperType?.Value is "Image" or "Video"; IsSolidColor = SelectedWallpaperType?.Value == "SolidColor"; } @@ -131,32 +146,65 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase partial void OnWallpaperPathChanged(string value) { - UpdatePreviewImage(value); + UpdatePreviewFromCurrentSelection(); if (_isInitializing) return; SaveWallpaper(); } + private void UpdatePreviewFromCurrentSelection() + { + if (!IsImage) + { + ClearPreviewImage(); + PreviewBrush = null; + return; + } + + UpdatePreviewImage(WallpaperPath); + } + private void UpdatePreviewImage(string path) { + var previousPreview = PreviewImage; if (string.IsNullOrWhiteSpace(path) || !System.IO.File.Exists(path)) { + previousPreview?.Dispose(); PreviewImage = null; + PreviewBrush = null; return; } try { using var stream = System.IO.File.OpenRead(path); - PreviewImage = new Bitmap(stream); + var bitmap = new Bitmap(stream); + PreviewImage = bitmap; + PreviewBrush = WallpaperImageBrushFactory.Create(bitmap, SelectedWallpaperPlacement?.Value); + previousPreview?.Dispose(); } catch { + previousPreview?.Dispose(); PreviewImage = null; + PreviewBrush = null; } } + private void ClearPreviewImage() + { + var previousPreview = PreviewImage; + PreviewImage = null; + PreviewBrush = null; + previousPreview?.Dispose(); + } + partial void OnSelectedWallpaperPlacementChanged(SelectionOption value) { + if (IsImage && PreviewImage is not null) + { + PreviewBrush = WallpaperImageBrushFactory.Create(PreviewImage, value?.Value); + } + if (_isInitializing || value is null) return; SaveWallpaper(); } @@ -169,14 +217,16 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase private void SaveWallpaper() { + var selectedType = SelectedWallpaperType?.Value ?? "Image"; + var selectedPlacement = SelectedWallpaperPlacement?.Value ?? WallpaperImageBrushFactory.Fill; var normalizedPath = SelectedWallpaperType?.Value == "SolidColor" || string.IsNullOrWhiteSpace(WallpaperPath) ? null : WallpaperPath; _settingsFacade.Wallpaper.Save(new WallpaperSettingsState( normalizedPath, - SelectedWallpaperType.Value, + selectedType, SelectedColor, - SelectedWallpaperPlacement.Value)); + selectedPlacement)); } private IReadOnlyList CreateWallpaperPlacements() @@ -221,6 +271,7 @@ public sealed partial class WallpaperSettingsPageViewModel : ViewModelBase WallpaperPlacementDescription = L("settings.wallpaper.placement_desc", "Adjust how the image fills the desktop."); ImportWallpaperButtonText = L("settings.wallpaper.pick_button", "Import Wallpaper"); FilePickerTitle = L("filepicker.title", "Select wallpaper"); + VideoModeHintText = L("settings.wallpaper.video_mode", "Video wallpaper uses automatic fill mode."); } private string L(string key, string fallback) diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index 0509fa0..3aa545e 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -99,12 +99,186 @@ public partial class MainWindow IReadOnlyList Components); private readonly record struct ComponentScaleRule(int WidthUnit, int HeightUnit, int MinScale); + private readonly record struct TaskbarProfilePopupMaterialPalette( + Color SurfaceColor, + Color OutlineColor, + Color AvatarSurfaceColor, + Color PrimaryTextColor, + Color AccentColor, + Color HoverColor, + Color PressedColor, + Color DividerColor); + + private void InitializeTaskbarProfileFlyout() + { + if (TaskbarProfileButton is null || TaskbarProfilePopup is null) + { + return; + } + + TaskbarProfilePopup.PlacementTarget = TaskbarProfileButton; + RefreshTaskbarProfilePresentation(); + } + + private void RefreshTaskbarProfilePresentation() + { + if (TaskbarProfileButton is null) + { + return; + } + + var profile = _currentUserProfileService.GetCurrentProfile(); + ApplyProfileAvatarVisual(TaskbarProfileAvatarImage, TaskbarProfileAvatarFallbackText, profile); + ApplyProfileAvatarVisual(TaskbarProfileHeaderAvatarImage, TaskbarProfileHeaderAvatarFallbackText, profile); + TaskbarProfileDisplayNameTextBlock.Text = profile.DisplayName; + TaskbarProfileSettingsActionTextBlock.Text = L("tooltip.open_settings", "Settings"); + TaskbarProfileDesktopEditActionTextBlock.Text = L("button.component_library", "Edit Desktop"); + ApplyTaskbarProfilePopupTheme(_appearanceThemeService.GetCurrent()); + + ToolTip.SetTip(TaskbarProfileButton, profile.DisplayName); + } + + private static void ApplyProfileAvatarVisual(Image? image, TextBlock? fallbackText, CurrentUserProfileSnapshot profile) + { + if (image is not null) + { + image.Source = profile.AvatarBitmap; + image.IsVisible = profile.AvatarBitmap is not null; + } + + if (fallbackText is not null) + { + fallbackText.Text = profile.FallbackMonogram; + fallbackText.IsVisible = profile.AvatarBitmap is null; + } + } + + private void ApplyTaskbarProfilePopupTheme(AppearanceThemeSnapshot snapshot) + { + if (TaskbarProfilePopupPanel is null) + { + return; + } + + var palette = BuildTaskbarProfilePopupMaterialPalette(snapshot); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupSurfaceBrush", palette.SurfaceColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupOutlineBrush", palette.OutlineColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupAvatarSurfaceBrush", palette.AvatarSurfaceColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupTextBrush", palette.PrimaryTextColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupAccentBrush", palette.AccentColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionHoverBrush", palette.HoverColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupActionPressedBrush", palette.PressedColor); + SetTaskbarProfilePopupBrush("TaskbarProfilePopupDividerBrush", palette.DividerColor); + } + + private void SetTaskbarProfilePopupBrush(string resourceKey, Color color) + { + TaskbarProfilePopupPanel.Resources[resourceKey] = new SolidColorBrush(color); + } + + private static TaskbarProfilePopupMaterialPalette BuildTaskbarProfilePopupMaterialPalette(AppearanceThemeSnapshot snapshot) + { + var primary = snapshot.MonetPalette.Primary.A > 0 + ? snapshot.MonetPalette.Primary + : snapshot.AccentColor; + if (primary == default) + { + primary = Color.Parse("#FF6750A4"); + } + + var neutral = snapshot.MonetPalette.Neutral.A > 0 + ? snapshot.MonetPalette.Neutral + : snapshot.IsNightMode + ? Color.Parse("#FF1A1F27") + : Color.Parse("#FFF7F9FD"); + var neutralVariant = snapshot.MonetPalette.NeutralVariant.A > 0 + ? snapshot.MonetPalette.NeutralVariant + : ColorMath.Blend(neutral, primary, snapshot.IsNightMode ? 0.20 : 0.10); + + var surfaceBase = snapshot.IsNightMode + ? Color.Parse("#FF141A22") + : Color.Parse("#FFFCFCFF"); + var surface = ColorMath.Blend(surfaceBase, neutral, snapshot.IsNightMode ? 0.52 : 0.46); + surface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.12 : 0.05); + + var outlineSeed = snapshot.IsNightMode + ? ColorMath.Blend(neutralVariant, Color.Parse("#FFFFFFFF"), 0.28) + : ColorMath.Blend(neutralVariant, Color.Parse("#FF111827"), 0.12); + var outline = Color.FromArgb( + snapshot.IsNightMode ? (byte)0x82 : (byte)0x38, + outlineSeed.R, + outlineSeed.G, + outlineSeed.B); + + var primaryTextPreferred = snapshot.IsNightMode + ? Color.Parse("#FFF4F7FB") + : Color.Parse("#FF14171B"); + var primaryText = ColorMath.EnsureContrast(primaryTextPreferred, surface, 7.0); + var accent = ColorMath.EnsureContrast(primary, surface, 3.0); + var avatarSurface = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.26 : 0.16); + var hover = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.20 : 0.10); + var pressed = ColorMath.Blend(surface, primary, snapshot.IsNightMode ? 0.30 : 0.18); + var divider = Color.FromArgb( + snapshot.IsNightMode ? (byte)0x44 : (byte)0x20, + outlineSeed.R, + outlineSeed.G, + outlineSeed.B); + + return new TaskbarProfilePopupMaterialPalette( + surface, + outline, + avatarSurface, + primaryText, + accent, + hover, + pressed, + divider); + } + + private void OnTaskbarProfileButtonClick(object? sender, RoutedEventArgs e) + { + _ = sender; + _ = e; + + if (TaskbarProfileButton is null || TaskbarProfilePopup is null) + { + return; + } + + if (TaskbarProfilePopup.IsOpen) + { + TaskbarProfilePopup.IsOpen = false; + return; + } + + RefreshTaskbarProfilePresentation(); + TaskbarProfilePopup.IsOpen = true; + } private void OnOpenComponentLibraryClick(object? sender, RoutedEventArgs e) { _ = sender; _ = e; + if (TaskbarProfilePopup is not null) + { + TaskbarProfilePopup.IsOpen = false; + } + ExecuteTaskbarDesktopEditAction(); + } + private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) + { + _ = sender; + _ = e; + if (TaskbarProfilePopup is not null) + { + TaskbarProfilePopup.IsOpen = false; + } + ExecuteTaskbarSettingsAction(); + } + + private void ExecuteTaskbarDesktopEditAction() + { if (_isComponentLibraryOpen) { CloseComponentLibraryWindow(reopenSettings: false); @@ -121,11 +295,8 @@ public partial class MainWindow OpenComponentLibraryWindow(); } - private void OnOpenSettingsClick(object? sender, RoutedEventArgs e) + private void ExecuteTaskbarSettingsAction() { - _ = sender; - _ = e; - if (_isComponentLibraryOpen) { CloseComponentLibraryWindow(reopenSettings: false); @@ -163,7 +334,6 @@ public partial class MainWindow _topStatusComponentIds.Add(BuiltInComponentIds.Clock); ApplyTopStatusComponentVisibility(); - UpdateWallpaperPreviewLayout(); PersistSettings(); } @@ -176,7 +346,6 @@ public partial class MainWindow _topStatusComponentIds.Remove(BuiltInComponentIds.Clock); ApplyTopStatusComponentVisibility(); - UpdateWallpaperPreviewLayout(); PersistSettings(); } @@ -257,25 +426,6 @@ public partial class MainWindow { TopStatusBarHost.IsVisible = hasVisibleTopStatusComponent; } - - if (WallpaperPreviewClockWidget is not null) - { - WallpaperPreviewClockWidget.IsVisible = showClock; - if (showClock) - { - WallpaperPreviewClockWidget.SetDisplayFormat(_clockDisplayFormat); - } - } - - if (WallpaperPreviewTopStatusBarHost is not null) - { - WallpaperPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent; - } - - if (GridPreviewTopStatusBarHost is not null) - { - GridPreviewTopStatusBarHost.IsVisible = hasVisibleTopStatusComponent; - } } private TaskbarContext GetCurrentTaskbarContext() @@ -286,19 +436,15 @@ public partial class MainWindow private void ApplyTaskbarActionVisibility(TaskbarContext context) { if (BackToWindowsButton is null || - OpenComponentLibraryButton is null || - OpenSettingsButton is null) + TaskbarProfileButton is null) { return; } var showMinimize = _pinnedTaskbarActions.Contains(TaskbarActionId.MinimizeToWindows); - var showSettings = true; - var showDesktopEdit = _isSettingsOpen; BackToWindowsButton.IsVisible = showMinimize; - OpenSettingsButton.IsVisible = showSettings; - OpenComponentLibraryButton.IsVisible = showDesktopEdit; + TaskbarProfileButton.IsVisible = true; if (TaskbarFixedActionsHost is not null) { @@ -307,7 +453,7 @@ public partial class MainWindow if (TaskbarSettingsActionHost is not null) { - TaskbarSettingsActionHost.IsVisible = showSettings || showDesktopEdit; + TaskbarSettingsActionHost.IsVisible = true; } UpdateOpenSettingsActionVisualState(); @@ -326,24 +472,10 @@ public partial class MainWindow private void UpdateOpenSettingsActionVisualState() { - if (OpenSettingsButtonTextBlock is null || OpenSettingsButton is null) - { - return; - } - - var showBackToDesktop = _isSettingsOpen; - var buttonText = L("settings.back_to_desktop", "Back to Desktop"); - OpenSettingsButtonTextBlock.IsVisible = showBackToDesktop; - OpenSettingsButtonTextBlock.Text = buttonText; - ToolTip.SetTip( - OpenSettingsButton, - showBackToDesktop - ? buttonText - : L("tooltip.open_settings", "Settings")); - var effectiveCellSize = _currentDesktopCellSize > 0 ? _currentDesktopCellSize : Math.Max(32, Math.Min(Bounds.Width, Bounds.Height) / Math.Max(1, _targetShortSideCells)); + RefreshTaskbarProfilePresentation(); ApplyWidgetSizing(effectiveCellSize); } diff --git a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs index ee8a9bf..f12ec94 100644 --- a/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs +++ b/LanMountainDesktop/Views/MainWindow.SettingsHardCut.Stubs.cs @@ -2,6 +2,7 @@ using System; using System.Globalization; using System.IO; using System.Linq; +using System.Threading.Tasks; using Avalonia; using Avalonia.Controls; using Avalonia.Interactivity; @@ -25,45 +26,6 @@ public partial class MainWindow private TextBlock? CurrentRenderBackendLabelTextBlock => this.FindControl("CurrentRenderBackendLabelTextBlock"); private TextBlock? CurrentRenderBackendValueTextBlock => this.FindControl("CurrentRenderBackendValueTextBlock"); private TextBlock? CurrentRenderBackendImplementationTextBlock => this.FindControl("CurrentRenderBackendImplementationTextBlock"); - private Slider? GridSizeSlider => this.FindControl("GridSizeSlider"); - private NumberBox? GridSizeNumberBox => this.FindControl("GridSizeNumberBox"); - private Slider? GridEdgeInsetSlider => this.FindControl("GridEdgeInsetSlider"); - private NumberBox? GridEdgeInsetNumberBox => this.FindControl("GridEdgeInsetNumberBox"); - private TextBlock? GridEdgeInsetComputedPxTextBlock => this.FindControl("GridEdgeInsetComputedPxTextBlock"); - private TextBlock? GridInfoTextBlock => this.FindControl("GridInfoTextBlock"); - private ComboBox? GridSpacingPresetComboBox => this.FindControl("GridSpacingPresetComboBox"); - private Border? GridPreviewHost => this.FindControl("GridPreviewHost"); - private Border? GridPreviewFrame => this.FindControl("GridPreviewFrame"); - private Border? GridPreviewViewport => this.FindControl("GridPreviewViewport"); - private Grid? GridPreviewGrid => this.FindControl("GridPreviewGrid"); - private Canvas? GridPreviewLinesCanvas => this.FindControl("GridPreviewLinesCanvas"); - private Border? GridPreviewTopStatusBarHost => this.FindControl("GridPreviewTopStatusBarHost"); - private StackPanel? GridPreviewTopStatusComponentsPanel => this.FindControl("GridPreviewTopStatusComponentsPanel"); - private Border? GridPreviewBottomTaskbarContainer => this.FindControl("GridPreviewBottomTaskbarContainer"); - private StackPanel? GridPreviewBackButtonVisual => this.FindControl("GridPreviewBackButtonVisual"); - private TextBlock? GridPreviewBackButtonTextBlock => this.FindControl("GridPreviewBackButtonTextBlock"); - private StackPanel? GridPreviewComponentLibraryVisual => this.FindControl("GridPreviewComponentLibraryVisual"); - private FluentIcons.Avalonia.FluentIcon? GridPreviewComponentLibraryIcon => this.FindControl("GridPreviewComponentLibraryIcon"); - private TextBlock? GridPreviewComponentLibraryTextBlock => this.FindControl("GridPreviewComponentLibraryTextBlock"); - private FluentIcons.Avalonia.SymbolIcon? GridPreviewSettingsButtonIcon => this.FindControl("GridPreviewSettingsButtonIcon"); - private Border? WallpaperPreviewHost => this.FindControl("WallpaperPreviewHost"); - private Border? WallpaperPreviewFrame => this.FindControl("WallpaperPreviewFrame"); - private Border? WallpaperPreviewViewport => this.FindControl("WallpaperPreviewViewport"); - private Grid? WallpaperPreviewGrid => this.FindControl("WallpaperPreviewGrid"); - private Border? WallpaperPreviewTopStatusBarHost => this.FindControl("WallpaperPreviewTopStatusBarHost"); - private StackPanel? WallpaperPreviewTopStatusComponentsPanel => this.FindControl("WallpaperPreviewTopStatusComponentsPanel"); - private Border? WallpaperPreviewBottomTaskbarContainer => this.FindControl("WallpaperPreviewBottomTaskbarContainer"); - private ClockWidget? WallpaperPreviewClockWidget => this.FindControl("WallpaperPreviewClockWidget"); - private StackPanel? WallpaperPreviewBackButtonVisual => this.FindControl("WallpaperPreviewBackButtonVisual"); - private TextBlock? WallpaperPreviewBackButtonTextBlock => this.FindControl("WallpaperPreviewBackButtonTextBlock"); - private StackPanel? WallpaperPreviewComponentLibraryVisual => this.FindControl("WallpaperPreviewComponentLibraryVisual"); - private TextBlock? WallpaperPreviewComponentLibraryTextBlock => this.FindControl("WallpaperPreviewComponentLibraryTextBlock"); - private FluentIcons.Avalonia.SymbolIcon? WallpaperPreviewSettingsButtonIcon => this.FindControl("WallpaperPreviewSettingsButtonIcon"); - private ComboBox? StatusBarSpacingModeComboBox => this.FindControl("StatusBarSpacingModeComboBox"); - private SettingsExpanderItem? StatusBarSpacingCustomPanel => this.FindControl("StatusBarSpacingCustomPanel"); - private Slider? StatusBarSpacingSlider => this.FindControl("StatusBarSpacingSlider"); - private NumberBox? StatusBarSpacingNumberBox => this.FindControl("StatusBarSpacingNumberBox"); - private TextBlock? StatusBarSpacingComputedPxTextBlock => this.FindControl("StatusBarSpacingComputedPxTextBlock"); private ComboBox? TimeZoneComboBox => this.FindControl("TimeZoneComboBox"); private SettingsExpander? LauncherHiddenItemsSettingsExpander => this.FindControl("LauncherHiddenItemsSettingsExpander"); private TextBlock? LauncherHiddenItemsEmptyTextBlock => this.FindControl("LauncherHiddenItemsEmptyTextBlock"); @@ -138,12 +100,12 @@ public partial class MainWindow { Title = L("app.title", "LanMountainDesktop"); BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows"); - OpenComponentLibraryTextBlock.Text = L("button.component_library", "Edit Desktop"); ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Widgets"); LauncherTitleTextBlock.Text = L("launcher.title", "App Launcher"); LauncherSubtitleTextBlock.Text = OperatingSystem.IsLinux() ? L("launcher.subtitle_linux", "Displays installed apps discovered from Linux desktop entries.") : L("launcher.subtitle", "Displays all apps and folders based on the Windows Start menu structure."); + RefreshTaskbarProfilePresentation(); UpdateCurrentRenderBackendStatus(); RenderLauncherHiddenItemsList(); @@ -238,46 +200,63 @@ public partial class MainWindow GlassEffectService.ApplyGlassResources(applicationResources, context); } - _defaultDesktopBackground = GetThemeBrush("AdaptiveWindowBackgroundBrush") - ?? GetThemeBrush("AdaptiveSurfaceBaseBrush"); + _defaultDesktopBackground = CreateNeutralWallpaperFallbackBrush(); } - private void TryRestoreWallpaper(string? savedWallpaperPath, string? type = null, string? color = null) + private void TryRestoreWallpaper( + string? savedWallpaperPath, + string? type = null, + string? color = null, + string? placement = null) { _wallpaperPath = string.IsNullOrWhiteSpace(savedWallpaperPath) ? null : savedWallpaperPath; - _wallpaperType = type ?? "Image"; - if (TryParseColor(color, out var parsedColor)) - { - _wallpaperSolidColor = parsedColor; - } + _wallpaperType = string.IsNullOrWhiteSpace(type) ? "Image" : type.Trim(); + _wallpaperPlacement = WallpaperImageBrushFactory.NormalizePlacement(placement); + _wallpaperSolidColor = TryParseColor(color, out var parsedColor) ? parsedColor : null; + _wallpaperVideoPath = null; + _wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured; _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; - if (_wallpaperType == "SolidColor") + if (string.Equals(_wallpaperType, "SolidColor", StringComparison.OrdinalIgnoreCase)) { _wallpaperMediaType = WallpaperMediaType.SolidColor; + _wallpaperDisplayState = _wallpaperSolidColor.HasValue + ? WallpaperDisplayState.CurrentValidWallpaper + : WallpaperDisplayState.NoWallpaperConfigured; return; } - if (string.IsNullOrWhiteSpace(_wallpaperPath) || !File.Exists(_wallpaperPath)) + if (string.IsNullOrWhiteSpace(_wallpaperPath)) { _wallpaperMediaType = WallpaperMediaType.None; return; } var extension = Path.GetExtension(_wallpaperPath); - if (SupportedVideoExtensions.Contains(extension) || _wallpaperType == "Video") + var requestedTypeIsVideo = string.Equals(_wallpaperType, "Video", StringComparison.OrdinalIgnoreCase); + if (SupportedVideoExtensions.Contains(extension) || requestedTypeIsVideo) { _wallpaperMediaType = WallpaperMediaType.Video; _wallpaperVideoPath = _wallpaperPath; + _wallpaperDisplayState = File.Exists(_wallpaperPath) + ? WallpaperDisplayState.CurrentValidWallpaper + : WallpaperDisplayState.TemporarilyUnavailable; return; } if (!SupportedImageExtensions.Contains(extension)) { - _wallpaperMediaType = WallpaperMediaType.None; - _wallpaperPath = null; + _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable; + return; + } + + if (!File.Exists(_wallpaperPath)) + { + _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable; return; } @@ -286,11 +265,13 @@ public partial class MainWindow using var stream = File.OpenRead(_wallpaperPath); _wallpaperBitmap = new Bitmap(stream); _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperDisplayState = WallpaperDisplayState.CurrentValidWallpaper; + CacheLastValidWallpaperBitmap(_wallpaperPath); } catch { - _wallpaperMediaType = WallpaperMediaType.None; - _wallpaperPath = null; + _wallpaperMediaType = WallpaperMediaType.Image; + _wallpaperDisplayState = WallpaperDisplayState.TemporarilyUnavailable; _wallpaperBitmap?.Dispose(); _wallpaperBitmap = null; } @@ -298,22 +279,42 @@ public partial class MainWindow private void ApplyWallpaperBrush() { + DesktopWallpaperImageLayer.Background = null; + DesktopWallpaperImageLayer.IsVisible = false; + if (_wallpaperMediaType == WallpaperMediaType.SolidColor && _wallpaperSolidColor.HasValue) { DesktopWallpaperLayer.Background = new SolidColorBrush(_wallpaperSolidColor.Value); + ApplyVideoWallpaperPosterVisibility(showPoster: false); return; } - if (_wallpaperMediaType == WallpaperMediaType.Image && _wallpaperBitmap is not null) + if (_wallpaperDisplayState == WallpaperDisplayState.CurrentValidWallpaper && + _wallpaperMediaType == WallpaperMediaType.Image && + _wallpaperBitmap is not null) { - DesktopWallpaperLayer.Background = new ImageBrush(_wallpaperBitmap) - { - Stretch = Stretch.UniformToFill - }; + DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush(); + DesktopWallpaperImageLayer.Background = WallpaperImageBrushFactory.Create(_wallpaperBitmap, _wallpaperPlacement); + DesktopWallpaperImageLayer.IsVisible = true; + ApplyVideoWallpaperPosterVisibility(showPoster: false); return; } - DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? Brushes.Transparent; + if (_wallpaperDisplayState == WallpaperDisplayState.TemporarilyUnavailable && + _lastValidWallpaperBitmap is not null && + !string.IsNullOrWhiteSpace(_wallpaperPath) && + string.Equals(_lastValidWallpaperPath, _wallpaperPath, StringComparison.OrdinalIgnoreCase)) + { + DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush(); + DesktopWallpaperImageLayer.Background = WallpaperImageBrushFactory.Create(_lastValidWallpaperBitmap, _wallpaperPlacement); + DesktopWallpaperImageLayer.IsVisible = true; + ApplyVideoWallpaperPosterVisibility(showPoster: false); + return; + } + + DesktopWallpaperLayer.Background = _defaultDesktopBackground ?? CreateNeutralWallpaperFallbackBrush(); + ApplyVideoWallpaperPosterVisibility( + showPoster: _wallpaperMediaType == WallpaperMediaType.Video && _videoWallpaperPosterBitmap is not null); } private void UpdateWallpaperDisplay() @@ -337,6 +338,7 @@ public partial class MainWindow { if (string.IsNullOrWhiteSpace(videoPath) || !File.Exists(videoPath)) { + ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null); return; } @@ -358,13 +360,25 @@ public partial class MainWindow videoView.IsVisible = true; } + if (!string.Equals(_videoWallpaperPosterPath, videoPath, StringComparison.OrdinalIgnoreCase)) + { + ApplyVideoWallpaperPosterVisibility(showPoster: false); + } + else + { + ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null); + } + if (!_videoWallpaperPlayer.IsPlaying) { _videoWallpaperPlayer.Play(); } + + TryCaptureVideoWallpaperPosterFrame(videoPath); } catch { + ApplyVideoWallpaperPosterVisibility(showPoster: _videoWallpaperPosterBitmap is not null); } } @@ -377,6 +391,7 @@ public partial class MainWindow _videoWallpaperPlayer?.Stop(); _wallpaperVideoPath = null; + ApplyVideoWallpaperPosterVisibility(showPoster: false); } private double CalculateCurrentBackgroundLuminance() @@ -514,13 +529,22 @@ public partial class MainWindow InitializeDesktopSurfaceState(layoutSnapshot); InitializeLauncherVisibilitySettings(launcherSnapshot); InitializeDesktopComponentPlacements(layoutSnapshot); - TryRestoreWallpaper(snapshot.WallpaperPath, snapshot.WallpaperType, snapshot.WallpaperColor); if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) { _selectedThemeColor = savedThemeColor; } - _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); + _isNightMode = snapshot.IsNightMode ?? _isNightMode; + _defaultDesktopBackground = CreateNeutralWallpaperFallbackBrush(); + TryRestoreWallpaper( + snapshot.WallpaperPath, + snapshot.WallpaperType, + snapshot.WallpaperColor, + snapshot.WallpaperPlacement); + if (!snapshot.IsNightMode.HasValue) + { + _isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold; + } ApplyNightModeState(_isNightMode, refreshPalettes: true); ApplyWallpaperBrush(); UpdateWallpaperDisplay(); @@ -536,6 +560,7 @@ public partial class MainWindow private AppSettingsSnapshot BuildAppSettingsSnapshot() { + var latestWallpaperState = _settingsFacade.Wallpaper.Get(); var latestWeatherState = _weatherSettingsService.Get(); var latestUpdateState = _updateSettingsService.Get(); var latestThemeState = _themeSettingsService.Get(); @@ -550,9 +575,12 @@ public partial class MainWindow SystemMaterialMode = latestThemeState.SystemMaterialMode, SelectedWallpaperSeed = latestThemeState.SelectedWallpaperSeed, UseSystemChrome = latestThemeState.UseSystemChrome, - WallpaperPath = _wallpaperPath, - WallpaperType = _wallpaperType, - WallpaperColor = _wallpaperSolidColor?.ToString(), + WallpaperPath = latestWallpaperState.WallpaperPath, + WallpaperType = latestWallpaperState.Type, + WallpaperColor = string.Equals(latestWallpaperState.Type, "SolidColor", StringComparison.OrdinalIgnoreCase) + ? latestWallpaperState.Color + : null, + WallpaperPlacement = latestWallpaperState.Placement, LanguageCode = _languageCode, TimeZoneId = _timeZoneService.CurrentTimeZone.Id, WeatherLocationMode = latestWeatherState.LocationMode, @@ -587,6 +615,141 @@ public partial class MainWindow }; } + private IBrush CreateNeutralWallpaperFallbackBrush() + { + var neutralColor = _isNightMode + ? Color.Parse("#FF0B0F14") + : Color.Parse("#FFF6F7F9"); + return new SolidColorBrush(neutralColor); + } + + private void CacheLastValidWallpaperBitmap(string wallpaperPath) + { + if (string.IsNullOrWhiteSpace(wallpaperPath) || !File.Exists(wallpaperPath)) + { + return; + } + + try + { + using var stream = File.OpenRead(wallpaperPath); + var cachedBitmap = new Bitmap(stream); + _lastValidWallpaperBitmap?.Dispose(); + _lastValidWallpaperBitmap = cachedBitmap; + _lastValidWallpaperPath = wallpaperPath; + } + catch + { + // Best effort cache only. + } + } + + private void ApplyVideoWallpaperPosterVisibility(bool showPoster) + { + if (DesktopVideoWallpaperImage is not { } posterImage) + { + return; + } + + if (!showPoster || + _videoWallpaperPosterBitmap is null || + !string.Equals(_videoWallpaperPosterPath, _wallpaperVideoPath, StringComparison.OrdinalIgnoreCase)) + { + posterImage.IsVisible = false; + return; + } + + posterImage.Source = _videoWallpaperPosterBitmap; + posterImage.IsVisible = true; + } + + private void TryCaptureVideoWallpaperPosterFrame(string videoPath) + { + if (_videoWallpaperPlayer is null || string.IsNullOrWhiteSpace(videoPath)) + { + return; + } + + _ = Task.Run(async () => + { + var snapshotPath = Path.Combine( + Path.GetTempPath(), + $"lanmountaindesktop-wallpaper-poster-{Guid.NewGuid():N}.png"); + + try + { + for (var attempt = 0; attempt < 12; attempt++) + { + await Task.Delay(250).ConfigureAwait(false); + + if (_wallpaperMediaType != WallpaperMediaType.Video || + !string.Equals(_wallpaperVideoPath, videoPath, StringComparison.OrdinalIgnoreCase) || + _videoWallpaperPlayer is null) + { + return; + } + + if (!_videoWallpaperPlayer.TakeSnapshot(0, snapshotPath, 640, 360)) + { + continue; + } + + if (!File.Exists(snapshotPath)) + { + continue; + } + + var fileInfo = new FileInfo(snapshotPath); + if (fileInfo.Length <= 0) + { + continue; + } + + Bitmap posterBitmap; + await using (var stream = File.OpenRead(snapshotPath)) + { + posterBitmap = new Bitmap(stream); + } + + await Dispatcher.UIThread.InvokeAsync(() => + { + if (_wallpaperMediaType != WallpaperMediaType.Video || + !string.Equals(_wallpaperVideoPath, videoPath, StringComparison.OrdinalIgnoreCase)) + { + posterBitmap.Dispose(); + return; + } + + _videoWallpaperPosterBitmap?.Dispose(); + _videoWallpaperPosterBitmap = posterBitmap; + _videoWallpaperPosterPath = videoPath; + ApplyVideoWallpaperPosterVisibility(showPoster: true); + }); + + return; + } + } + catch + { + // Best effort poster capture only. + } + finally + { + try + { + if (File.Exists(snapshotPath)) + { + File.Delete(snapshotPath); + } + } + catch + { + // Best effort cleanup only. + } + } + }); + } + private DesktopLayoutSettingsSnapshot BuildDesktopLayoutSettingsSnapshot() { return new DesktopLayoutSettingsSnapshot diff --git a/LanMountainDesktop/Views/MainWindow.axaml b/LanMountainDesktop/Views/MainWindow.axaml index d7dc046..0362f53 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml +++ b/LanMountainDesktop/Views/MainWindow.axaml @@ -4,6 +4,7 @@ xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:fi="using:FluentIcons.Avalonia" xmlns:ic="using:FluentIcons.Avalonia.Fluent" + xmlns:mi="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:comp="using:LanMountainDesktop.Views.Components" xmlns:vlc="clr-namespace:LibVLCSharp.Avalonia;assembly=LibVLCSharp.Avalonia" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" @@ -13,7 +14,6 @@ d:DesignHeight="720" x:Class="LanMountainDesktop.Views.MainWindow" x:DataType="vm:MainWindowViewModel" - Icon="/Assets/avalonia-logo.ico" WindowState="FullScreen" SystemDecorations="None" CanResize="False" @@ -25,6 +25,75 @@ + + + + + + + + + + + + + + + + + + + + + + Background="#FFF6F7F9" /> - + + + - - - + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LanMountainDesktop/Views/MainWindow.axaml.cs b/LanMountainDesktop/Views/MainWindow.axaml.cs index 52b3abd..2e10eec 100644 --- a/LanMountainDesktop/Views/MainWindow.axaml.cs +++ b/LanMountainDesktop/Views/MainWindow.axaml.cs @@ -12,7 +12,6 @@ using FluentAvalonia.UI.Controls; using Avalonia.Layout; using Avalonia.Media; using Avalonia.Media.Imaging; -using Line = Avalonia.Controls.Shapes.Line; using Avalonia.Platform; using Avalonia.Platform.Storage; using Avalonia.Styling; @@ -32,15 +31,6 @@ namespace LanMountainDesktop.Views; public partial class MainWindow : Window, ISettingsWindowAnchorProvider { - private enum WallpaperPlacement - { - Fill, - Fit, - Stretch, - Center, - Tile - } - private enum WallpaperMediaType { None, @@ -49,6 +39,13 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider SolidColor } + private enum WallpaperDisplayState + { + NoWallpaperConfigured, + TemporarilyUnavailable, + CurrentValidWallpaper + } + private enum WeatherLocationMode { CitySearch, @@ -62,7 +59,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private const int MaxEdgeInsetPercent = 30; private const int DefaultEdgeInsetPercent = 18; private static readonly int SettingsTransitionDurationMs = (int)FluttermotionToken.Page.TotalMilliseconds; - private const double WallpaperPreviewMaxWidth = 520; private const double LightBackgroundLuminanceThreshold = 0.57; private const string TaskbarLayoutBottomFullRowMacStyle = "BottomFullRowMacStyle"; private static readonly HashSet SupportedImageExtensions = new(StringComparer.OrdinalIgnoreCase) @@ -79,6 +75,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider ]; private readonly ISettingsFacadeService _settingsFacade = HostSettingsFacadeProvider.GetOrCreate(); private readonly IAppearanceThemeService _appearanceThemeService = HostAppearanceThemeProvider.GetOrCreate(); + private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate(); + private readonly ICurrentUserProfileService _currentUserProfileService = HostCurrentUserProfileProvider.GetOrCreate(); private readonly IGridSettingsService _gridSettingsService; private readonly IThemeAppearanceService _themeSettingsService; private readonly IWeatherSettingsService _weatherSettingsService; @@ -114,14 +112,19 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private bool _suppressTimeZoneSelectionEvents; private bool _suppressWeatherLocationEvents; private bool _suppressSettingsPersistence; - private bool _isUpdatingWallpaperPreviewLayout; private bool _isComponentLibraryOpen; private Border? _selectedDesktopComponentHost; private bool _reopenSettingsAfterComponentLibraryClose; private TranslateTransform? _settingsContentPanelTransform; private IBrush? _defaultDesktopBackground; private Bitmap? _wallpaperBitmap; + private Bitmap? _lastValidWallpaperBitmap; + private string? _lastValidWallpaperPath; + private Bitmap? _videoWallpaperPosterBitmap; + private string? _videoWallpaperPosterPath; private WallpaperMediaType _wallpaperMediaType; + private WallpaperDisplayState _wallpaperDisplayState = WallpaperDisplayState.NoWallpaperConfigured; + private string _wallpaperPlacement = WallpaperImageBrushFactory.Fill; private string? _wallpaperVideoPath; private string _wallpaperType = "Image"; private Color? _wallpaperSolidColor; @@ -136,13 +139,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private IntPtr _desktopVideoFrameBufferPtr; private byte[]? _desktopVideoStagingBuffer; private WriteableBitmap? _desktopVideoBitmap; - private WriteableBitmap? _wallpaperPreviewSnapshotBitmap; private int _desktopVideoFrameWidth; private int _desktopVideoFrameHeight; private int _desktopVideoFramePitch; private int _desktopVideoFrameBufferSize; private int _desktopVideoFrameDirtyFlag; - private bool _wallpaperPreviewSnapshotPending; private string? _wallpaperPath; private string _wallpaperStatus = "Current background uses solid color."; private IReadOnlyList _recommendedColors = Array.Empty(); @@ -154,9 +155,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private string _gridSpacingPreset = "Relaxed"; private string _statusBarSpacingMode = "Relaxed"; private int _statusBarCustomSpacingPercent = 12; - private bool _suppressGridSpacingEvents; - private bool _suppressGridInsetEvents; - private bool _suppressStatusBarSpacingEvents; private int _desktopEdgeInsetPercent = DefaultEdgeInsetPercent; private string _taskbarLayoutMode = TaskbarLayoutBottomFullRowMacStyle; private string _languageCode = "zh-CN"; @@ -179,7 +177,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider private bool _isWeatherPreviewInProgress; private ClockDisplayFormat _clockDisplayFormat = ClockDisplayFormat.HourMinuteSecond; private bool _externalSettingsReloadPending; - private double CurrentDesktopPitch => _currentDesktopCellSize + _currentDesktopCellGap; public MainWindow() @@ -196,6 +193,8 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _weatherDataService = _weatherSettingsService.GetWeatherInfoService(); InitializeComponent(); + Icon = _appLogoService.CreateWindowIcon(); + InitializeTaskbarProfileFlyout(); _componentRuntimeRegistry = DesktopComponentRegistryFactory.CreateRuntimeRegistry( _componentRegistry, pluginRuntimeService, @@ -276,7 +275,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _statusBarSpacingMode = NormalizeStatusBarSpacingMode(snapshot.StatusBarSpacingMode); _statusBarCustomSpacingPercent = Math.Clamp(snapshot.StatusBarCustomSpacingPercent, 0, 30); - _defaultDesktopBackground = DesktopWallpaperLayer.Background; ApplyTaskbarSettings(snapshot); InitializeLocalization(snapshot.LanguageCode); InitializeWeatherSettings(snapshot); @@ -288,16 +286,28 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider InitializeDesktopComponentPlacements(desktopLayoutSnapshot); InitializeSettingsIcons(); - TryRestoreWallpaper(snapshot.WallpaperPath, snapshot.WallpaperType, snapshot.WallpaperColor); - ApplyWallpaperBrush(); - UpdateWallpaperDisplay(); - if (TryParseColor(snapshot.ThemeColor, out var savedThemeColor)) { _selectedThemeColor = savedThemeColor; } - _isNightMode = snapshot.IsNightMode ?? (CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold); + _isNightMode = snapshot.IsNightMode + ?? (Application.Current?.ActualThemeVariant == ThemeVariant.Dark); + _defaultDesktopBackground = CreateNeutralWallpaperFallbackBrush(); + + TryRestoreWallpaper( + snapshot.WallpaperPath, + snapshot.WallpaperType, + snapshot.WallpaperColor, + snapshot.WallpaperPlacement); + ApplyWallpaperBrush(); + UpdateWallpaperDisplay(); + + if (!snapshot.IsNightMode.HasValue) + { + _isNightMode = CalculateCurrentBackgroundLuminance() < LightBackgroundLuminanceThreshold; + } + ApplyNightModeState(_isNightMode, refreshPalettes: true); ApplyLocalization(); DesktopHost.SizeChanged += OnDesktopHostSizeChanged; @@ -331,8 +341,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _videoWallpaperPlayer = null; _desktopVideoFrameRefreshTimer?.Stop(); _desktopVideoFrameRefreshTimer = null; - _wallpaperPreviewSnapshotBitmap?.Dispose(); - _wallpaperPreviewSnapshotBitmap = null; + _videoWallpaperPosterBitmap?.Dispose(); + _videoWallpaperPosterBitmap = null; + _videoWallpaperPosterPath = null; + _lastValidWallpaperBitmap?.Dispose(); + _lastValidWallpaperBitmap = null; _libVlc?.Dispose(); _libVlc = null; if (_recommendationInfoService is IDisposable recommendationServiceDisposable) @@ -382,449 +395,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider PersistSettings(); } - private void OnWallpaperPreviewHostSizeChanged(object? sender, SizeChangedEventArgs e) - { - UpdateWallpaperPreviewLayout(); - } - - private void OnGridPreviewHostSizeChanged(object? sender, SizeChangedEventArgs e) - { - UpdateGridPreviewLayout(); - } - - private void OnGridSizeSliderChanged(object? sender, RoutedEventArgs e) - { - if (GridSizeSlider is null || GridSizeNumberBox is null) - { - return; - } - - var sliderValue = (int)Math.Round(GridSizeSlider.Value); - if (Math.Abs(GridSizeNumberBox.Value - sliderValue) > double.Epsilon) - { - GridSizeNumberBox.Value = sliderValue; - } - UpdateGridPreviewLayout(); - } - - private void OnGridSizeNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) - { - if (GridSizeSlider is null || GridSizeNumberBox is null) - { - return; - } - - var numberBoxValue = (int)Math.Round(GridSizeNumberBox.Value); - if (Math.Abs(GridSizeSlider.Value - numberBoxValue) > double.Epsilon) - { - GridSizeSlider.Value = numberBoxValue; - } - UpdateGridPreviewLayout(); - } - - private void OnGridEdgeInsetSliderChanged(object? sender, RoutedEventArgs e) - { - if (GridEdgeInsetSlider is null) - { - return; - } - - if (_suppressGridInsetEvents) - { - return; - } - - var value = (int)Math.Round(GridEdgeInsetSlider.Value); - SetPendingGridEdgeInsetPercent(value, updateSlider: false, updateNumberBox: true); - UpdateGridPreviewLayout(); - } - - private void OnGridEdgeInsetNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) - { - if (GridEdgeInsetNumberBox is null) - { - return; - } - - if (_suppressGridInsetEvents) - { - return; - } - - var value = (int)Math.Round(GridEdgeInsetNumberBox.Value); - SetPendingGridEdgeInsetPercent(value, updateSlider: true, updateNumberBox: false); - UpdateGridPreviewLayout(); - } - - private void SetPendingGridEdgeInsetPercent(int percent, bool updateSlider, bool updateNumberBox) - { - var clamped = Math.Clamp(percent, MinEdgeInsetPercent, MaxEdgeInsetPercent); - - _suppressGridInsetEvents = true; - try - { - if (updateSlider && Math.Abs(GridEdgeInsetSlider.Value - clamped) > double.Epsilon) - { - GridEdgeInsetSlider.Value = clamped; - } - - if (updateNumberBox && Math.Abs(GridEdgeInsetNumberBox.Value - clamped) > double.Epsilon) - { - GridEdgeInsetNumberBox.Value = clamped; - } - } - finally - { - _suppressGridInsetEvents = false; - } - } - - private void OnGridSpacingPresetSelectionChanged(object? sender, SelectionChangedEventArgs e) - { - if (_suppressGridSpacingEvents) - { - return; - } - - UpdateGridPreviewLayout(); - } - - private void OnStatusBarSpacingModeChanged(object? sender, SelectionChangedEventArgs e) - { - if (StatusBarSpacingModeComboBox is null) - { - return; - } - - if (_suppressStatusBarSpacingEvents) - { - return; - } - - _statusBarSpacingMode = NormalizeStatusBarSpacingMode( - TryGetSelectedComboBoxTag(StatusBarSpacingModeComboBox) ?? _statusBarSpacingMode); - - StatusBarSpacingCustomPanel.IsVisible = string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase); - - ApplyDesktopStatusBarComponentSpacing(); - UpdateWallpaperPreviewLayout(); - UpdateGridPreviewLayout(); - SchedulePersistSettings(); - } - - private void OnStatusBarSpacingSliderChanged(object? sender, RangeBaseValueChangedEventArgs e) - { - if (StatusBarSpacingSlider is null) - { - return; - } - - if (_suppressStatusBarSpacingEvents) - { - return; - } - - var percent = (int)Math.Round(StatusBarSpacingSlider.Value); - SetStatusBarCustomSpacingPercent(percent, updateSlider: false, updateNumberBox: true); - - if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase)) - { - ApplyDesktopStatusBarComponentSpacing(); - UpdateWallpaperPreviewLayout(); - UpdateGridPreviewLayout(); - } - - SchedulePersistSettings(); - } - - private void OnStatusBarSpacingNumberBoxChanged(object? sender, NumberBoxValueChangedEventArgs e) - { - if (StatusBarSpacingNumberBox is null) - { - return; - } - - if (_suppressStatusBarSpacingEvents) - { - return; - } - - var percent = (int)Math.Round(StatusBarSpacingNumberBox.Value); - SetStatusBarCustomSpacingPercent(percent, updateSlider: true, updateNumberBox: false); - - if (string.Equals(_statusBarSpacingMode, "Custom", StringComparison.OrdinalIgnoreCase)) - { - ApplyDesktopStatusBarComponentSpacing(); - UpdateWallpaperPreviewLayout(); - UpdateGridPreviewLayout(); - } - - SchedulePersistSettings(); - } - - private void SetStatusBarCustomSpacingPercent(int percent, bool updateSlider, bool updateNumberBox) - { - percent = Math.Clamp(percent, 0, 30); - _statusBarCustomSpacingPercent = percent; - - _suppressStatusBarSpacingEvents = true; - try - { - if (updateSlider && Math.Abs(StatusBarSpacingSlider.Value - percent) > double.Epsilon) - { - StatusBarSpacingSlider.Value = percent; - } - - if (updateNumberBox && Math.Abs(StatusBarSpacingNumberBox.Value - percent) > double.Epsilon) - { - StatusBarSpacingNumberBox.Value = percent; - } - } - finally - { - _suppressStatusBarSpacingEvents = false; - } - } - - private void UpdateGridPreviewLayout() - { - if (GridPreviewFrame is null || - GridPreviewHost is null || - GridPreviewViewport is null || - GridPreviewGrid is null || - GridPreviewLinesCanvas is null || - GridSizeSlider is null) - { - return; - } - - var previewShortSideCells = (int)Math.Round(GridSizeSlider.Value); - if (previewShortSideCells < MinShortSideCells || previewShortSideCells > MaxShortSideCells) - { - previewShortSideCells = _targetShortSideCells; - } - - var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); - var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); - var aspectRatio = desktopWidth / desktopHeight; - - var availableWidth = Math.Max(100, GridPreviewHost.Bounds.Width); - - var framePadding = GridPreviewFrame.Padding; - var horizontalPadding = framePadding.Left + framePadding.Right; - var verticalPadding = framePadding.Top + framePadding.Bottom; - - var gridPreviewWidth = availableWidth; - var gridPreviewHeight = gridPreviewWidth / aspectRatio; - - GridPreviewFrame.Width = gridPreviewWidth; - GridPreviewFrame.Height = gridPreviewHeight; - - var innerWidth = Math.Max(1, gridPreviewWidth - horizontalPadding); - var innerHeight = Math.Max(1, gridPreviewHeight - verticalPadding); - var preset = _gridSettingsService.NormalizeSpacingPreset(TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); - var gapRatio = _gridSettingsService.ResolveGapRatio(preset); - var pendingEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); - var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, previewShortSideCells, pendingEdgeInsetPercent); - var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, previewShortSideCells, gapRatio, edgeInset); - if (gridMetrics.CellSize <= 0) - { - return; - } - - var inset = new Thickness(gridMetrics.EdgeInsetPx); - GridPreviewGrid.Margin = inset; - GridPreviewGrid.RowSpacing = gridMetrics.GapPx; - GridPreviewGrid.ColumnSpacing = gridMetrics.GapPx; - GridPreviewGrid.Width = gridMetrics.GridWidthPx; - GridPreviewGrid.Height = gridMetrics.GridHeightPx; - - GridPreviewLinesCanvas.Margin = inset; - - GridPreviewGrid.RowDefinitions.Clear(); - GridPreviewGrid.ColumnDefinitions.Clear(); - - for (var row = 0; row < gridMetrics.RowCount; row++) - { - GridPreviewGrid.RowDefinitions.Add( - new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); - } - - for (var col = 0; col < gridMetrics.ColumnCount; col++) - { - GridPreviewGrid.ColumnDefinitions.Add( - new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); - } - - PlaceStatusBarComponent( - GridPreviewTopStatusBarHost, - column: 0, - requestedColumnSpan: gridMetrics.ColumnCount, - totalColumns: gridMetrics.ColumnCount); - - var taskbarRow = gridMetrics.RowCount - 1; - Grid.SetRow(GridPreviewBottomTaskbarContainer, taskbarRow); - Grid.SetColumn(GridPreviewBottomTaskbarContainer, 0); - Grid.SetRowSpan(GridPreviewBottomTaskbarContainer, 1); - Grid.SetColumnSpan(GridPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); - - ApplyGridPreviewWidgetSizing(gridMetrics.CellSize); - ApplyStatusBarComponentSpacingForPanel(GridPreviewTopStatusComponentsPanel, gridMetrics.CellSize); - UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize); - - if (GridInfoTextBlock is not null) - { - GridInfoTextBlock.Text = Lf( - "settings.grid.info_format", - "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", - gridMetrics.ColumnCount, - gridMetrics.RowCount, - gridMetrics.CellSize); - } - - DrawGridPreviewLines(gridMetrics); - } - - private void DrawGridPreviewLines(DesktopGridMetrics gridMetrics) - { - if (GridPreviewLinesCanvas is null || GridPreviewViewport is null || GridPreviewGrid is null) - { - return; - } - - var viewportBackground = GridPreviewViewport.Background as SolidColorBrush; - var backgroundColor = viewportBackground?.Color ?? Color.Parse("#30111827"); - var luminance = CalculateRelativeLuminance(backgroundColor); - var lineColor = luminance >= LightBackgroundLuminanceThreshold - ? Color.Parse("#80000000") - : Color.Parse("#80FFFFFF"); - - GridPreviewLinesCanvas.Children.Clear(); - - var cellSize = gridMetrics.CellSize; - var pitch = gridMetrics.Pitch; - var gridWidth = gridMetrics.GridWidthPx; - var gridHeight = gridMetrics.GridHeightPx; - - GridPreviewLinesCanvas.Width = gridWidth; - GridPreviewLinesCanvas.Height = gridHeight; - - var dashLength = cellSize * 0.3; - var gapLength = cellSize * 0.2; - - for (var row = 0; row <= gridMetrics.RowCount; row++) - { - var y = row == gridMetrics.RowCount ? gridHeight : row * pitch; - var line = new Line - { - StartPoint = new Point(0, y), - EndPoint = new Point(gridWidth, y), - Stroke = new SolidColorBrush(lineColor), - StrokeThickness = 1, - StrokeDashArray = new Avalonia.Collections.AvaloniaList { dashLength, gapLength }, - IsHitTestVisible = false - }; - GridPreviewLinesCanvas.Children.Add(line); - } - - for (var col = 0; col <= gridMetrics.ColumnCount; col++) - { - var x = col == gridMetrics.ColumnCount ? gridWidth : col * pitch; - var line = new Line - { - StartPoint = new Point(x, 0), - EndPoint = new Point(x, gridHeight), - Stroke = new SolidColorBrush(lineColor), - StrokeThickness = 1, - StrokeDashArray = new Avalonia.Collections.AvaloniaList { dashLength, gapLength }, - IsHitTestVisible = false - }; - GridPreviewLinesCanvas.Children.Add(line); - } - } - - private void ApplyGridPreviewWidgetSizing(double cellSize) - { - var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 30); - var iconSize = Math.Clamp(cellSize * 0.35, 8, 16); - - GridPreviewTopStatusBarHost.Padding = new Thickness(0); - GridPreviewBottomTaskbarContainer.Margin = new Thickness(0); - GridPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 16, 32)); - GridPreviewBottomTaskbarContainer.Padding = new Thickness(Math.Clamp(cellSize * 0.06, 1, 4)); - - GridPreviewBackButtonTextBlock.FontSize = Math.Clamp(cellSize * 0.19, 5, 13); - GridPreviewComponentLibraryTextBlock.FontSize = Math.Clamp(cellSize * 0.18, 5, 12); - GridPreviewComponentLibraryIcon.FontSize = iconSize; - GridPreviewBackButtonVisual.MinHeight = previewTaskbarCell; - GridPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120); - GridPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell; - GridPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110); - GridPreviewSettingsButtonIcon.Width = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); - GridPreviewSettingsButtonIcon.Height = Math.Clamp(previewTaskbarCell * 0.42, 6, 14); - } - - private void OnApplyGridSizeClick(object? sender, RoutedEventArgs e) - { - if (GridSizeNumberBox is null || GridSizeSlider is null) - { - return; - } - - _gridSpacingPreset = _gridSettingsService.NormalizeSpacingPreset( - TryGetSelectedComboBoxTag(GridSpacingPresetComboBox) ?? _gridSpacingPreset); - _desktopEdgeInsetPercent = ResolvePendingGridEdgeInsetPercent(); - - var requested = (int)Math.Round(GridSizeNumberBox.Value); - if (requested <= 0) - { - requested = _targetShortSideCells; - } - - _targetShortSideCells = Math.Clamp(requested, MinShortSideCells, MaxShortSideCells); - - if (Math.Abs(GridSizeNumberBox.Value - _targetShortSideCells) > double.Epsilon) - { - GridSizeNumberBox.Value = _targetShortSideCells; - } - - if (Math.Abs(GridSizeSlider.Value - _targetShortSideCells) > double.Epsilon) - { - GridSizeSlider.Value = _targetShortSideCells; - } - - SetPendingGridEdgeInsetPercent(_desktopEdgeInsetPercent, updateSlider: true, updateNumberBox: true); - - RebuildDesktopGrid(); - PersistSettings(); - } - - private void OnClockFormatChanged(object? sender, RoutedEventArgs e) - { - if (sender is not RadioButton radioButton || radioButton.Tag is not string formatTag) - { - return; - } - - if (radioButton.IsChecked != true) - { - return; - } - - _clockDisplayFormat = formatTag == "Hm" - ? ClockDisplayFormat.HourMinute - : ClockDisplayFormat.HourMinuteSecond; - - if (ClockWidget is ClockWidget clock) - { - clock.SetDisplayFormat(_clockDisplayFormat); - } - - ApplyTopStatusComponentVisibility(); - UpdateWallpaperPreviewLayout(); - PersistSettings(); - } - private void RebuildDesktopGrid() { var hostWidth = DesktopHost.Bounds.Width; @@ -839,7 +409,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _currentDesktopCellSize = gridMetrics.CellSize; _currentDesktopCellGap = gridMetrics.GapPx; _currentDesktopEdgeInset = gridMetrics.EdgeInsetPx; - UpdateGridEdgeInsetComputedPxText(gridMetrics.CellSize); DesktopGrid.RowDefinitions.Clear(); DesktopGrid.ColumnDefinitions.Clear(); @@ -878,24 +447,11 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider ApplyDesktopStatusBarComponentSpacing(); UpdateDesktopSurfaceLayout(gridMetrics); UpdateSettingsViewportInsets(gridMetrics.CellSize); - - if (GridInfoTextBlock is not null) - { - GridInfoTextBlock.Text = Lf( - "settings.grid.info_format", - "Grid: {0} cols x {1} rows | cell {2:F1}px (1:1)", - gridMetrics.ColumnCount, - gridMetrics.RowCount, - gridMetrics.CellSize); - } - - UpdateWallpaperPreviewLayout(); } private void ApplyDesktopStatusBarComponentSpacing() { ApplyStatusBarComponentSpacingForPanel(TopStatusComponentsPanel, _currentDesktopCellSize); - UpdateStatusBarSpacingComputedPxText(_currentDesktopCellSize); } private int ResolveStatusBarSpacingPercent() @@ -920,47 +476,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider panel.Spacing = spacingPx; } - private void UpdateStatusBarSpacingComputedPxText(double cellSize) - { - if (StatusBarSpacingComputedPxTextBlock is null) - { - return; - } - - var percent = ResolveStatusBarSpacingPercent(); - var spacingPx = Math.Max(0, cellSize) * (percent / 100d); - StatusBarSpacingComputedPxTextBlock.Text = Lf( - "settings.status_bar.spacing_custom_px_format", - ">= {0:F1}px", - spacingPx); - } - - private int ResolvePendingGridEdgeInsetPercent() - { - if (GridEdgeInsetNumberBox is null) - { - return _desktopEdgeInsetPercent; - } - - var pending = (int)Math.Round(GridEdgeInsetNumberBox.Value); - return Math.Clamp(pending, MinEdgeInsetPercent, MaxEdgeInsetPercent); - } - - private void UpdateGridEdgeInsetComputedPxText(double cellSize) - { - if (GridEdgeInsetComputedPxTextBlock is null) - { - return; - } - - var percent = ResolvePendingGridEdgeInsetPercent(); - var insetPx = Math.Clamp(Math.Max(0, cellSize) * (percent / 100d), 0, 80); - GridEdgeInsetComputedPxTextBlock.Text = Lf( - "settings.grid.edge_inset_px_format", - "{0:F1}px", - insetPx); - } - private static string NormalizeStatusBarSpacingMode(string? value) { return value switch @@ -971,16 +486,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider }; } - private static string? TryGetSelectedComboBoxTag(ComboBox? comboBox) - { - if (comboBox?.SelectedItem is ComboBoxItem item) - { - return item.Tag?.ToString(); - } - - return comboBox?.SelectedItem?.ToString(); - } - private static int ClampComponentSpan(int requestedSpan, int axisCellCount) { return Math.Clamp(requestedSpan, 1, Math.Max(1, axisCellCount)); @@ -1036,25 +541,21 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider BackToWindowsTextBlock.FontSize = taskbarTextSize; SetButtonContentSpacing(BackToWindowsButton, buttonContentSpacing); - OpenSettingsButton.Margin = new Thickness(0); - OpenSettingsButton.Padding = taskbarButtonPadding; - OpenSettingsButton.FontSize = taskbarTextSize; - OpenSettingsButton.MinHeight = taskbarCellHeight; - OpenSettingsButton.MinWidth = OpenSettingsButtonTextBlock.IsVisible - ? Math.Clamp(taskbarCellHeight * 2.35, 100, 340) - : Math.Clamp(taskbarCellHeight * 1.10, 48, 88); - OpenSettingsIcon.FontSize = taskbarIconSize; - OpenSettingsButtonTextBlock.FontSize = taskbarTextSize; - SetButtonContentSpacing(OpenSettingsButton, buttonContentSpacing); - - OpenComponentLibraryButton.Margin = new Thickness(0); - OpenComponentLibraryButton.Padding = taskbarButtonPadding; - OpenComponentLibraryButton.FontSize = taskbarTextSize; - OpenComponentLibraryButton.MinHeight = taskbarCellHeight; - OpenComponentLibraryButton.MinWidth = Math.Clamp(taskbarCellHeight * 2.15, 92, 320); - OpenComponentLibraryIcon.FontSize = taskbarIconSize; - OpenComponentLibraryTextBlock.FontSize = taskbarTextSize; - SetButtonContentSpacing(OpenComponentLibraryButton, buttonContentSpacing); + TaskbarProfileButton.Margin = new Thickness(0); + TaskbarProfileButton.Padding = new Thickness(0); + TaskbarProfileButton.MinHeight = taskbarCellHeight; + TaskbarProfileButton.MinWidth = taskbarCellHeight; + TaskbarProfileButton.Width = taskbarCellHeight; + TaskbarProfileButton.Height = taskbarCellHeight; + + var avatarSize = Math.Clamp(taskbarCellHeight * 0.82, 28, 60); + var avatarRadius = avatarSize / 2d; + TaskbarProfileAvatarBorder.Width = avatarSize; + TaskbarProfileAvatarBorder.Height = avatarSize; + TaskbarProfileAvatarBorder.CornerRadius = new CornerRadius(avatarRadius); + TaskbarProfileAvatarImage.Width = avatarSize; + TaskbarProfileAvatarImage.Height = avatarSize; + TaskbarProfileAvatarFallbackText.FontSize = Math.Clamp(avatarSize * 0.34, 10, 22); UpdateComponentLibraryLayout(cellSize); } @@ -1093,141 +594,6 @@ public partial class MainWindow : Window, ISettingsWindowAnchorProvider _ = cellSize; } - private void UpdateWallpaperPreviewLayout() - { - if (WallpaperPreviewFrame is null || - WallpaperPreviewHost is null || - WallpaperPreviewViewport is null || - WallpaperPreviewGrid is null) - { - return; - } - - if (_isUpdatingWallpaperPreviewLayout) - { - return; - } - - _isUpdatingWallpaperPreviewLayout = true; - try - { - var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); - var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); - var aspectRatio = desktopWidth / desktopHeight; - - var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width); - var availableHeight = WallpaperPreviewHost.Bounds.Height; - // During initial measure, host height can be too small and cause the preview to collapse. - // Ignore tiny heights so width-driven sizing can stabilize first. - if (availableHeight < 120) - { - availableHeight = double.PositiveInfinity; - } - - var framePadding = WallpaperPreviewFrame.Padding; - var horizontalPadding = framePadding.Left + framePadding.Right; - var verticalPadding = framePadding.Top + framePadding.Bottom; - - var previewWidth = Math.Min(availableWidth, WallpaperPreviewMaxWidth); - var previewHeight = previewWidth / aspectRatio; - if (double.IsFinite(availableHeight) && previewHeight > availableHeight) - { - previewHeight = availableHeight; - previewWidth = previewHeight * aspectRatio; - } - - WallpaperPreviewFrame.Width = previewWidth; - WallpaperPreviewFrame.Height = previewHeight; - - - - var innerWidth = Math.Max(1, previewWidth - horizontalPadding); - var innerHeight = Math.Max(1, previewHeight - verticalPadding); - var gapRatio = _gridSettingsService.ResolveGapRatio(_gridSpacingPreset); - var edgeInset = _gridSettingsService.CalculateEdgeInset(innerWidth, innerHeight, _targetShortSideCells, _desktopEdgeInsetPercent); - var gridMetrics = _gridSettingsService.CalculateGridMetrics(innerWidth, innerHeight, _targetShortSideCells, gapRatio, edgeInset); - if (gridMetrics.CellSize <= 0) - { - return; - } - - WallpaperPreviewGrid.Margin = new Thickness(gridMetrics.EdgeInsetPx); - WallpaperPreviewGrid.RowSpacing = gridMetrics.GapPx; - WallpaperPreviewGrid.ColumnSpacing = gridMetrics.GapPx; - WallpaperPreviewGrid.Width = gridMetrics.GridWidthPx; - WallpaperPreviewGrid.Height = gridMetrics.GridHeightPx; - - // This can be triggered by layout changes; always rebuild the preview grid definitions - // to avoid definitions accumulating and shifting overlay components out of place. - WallpaperPreviewGrid.RowDefinitions.Clear(); - WallpaperPreviewGrid.ColumnDefinitions.Clear(); - - for (var row = 0; row < gridMetrics.RowCount; row++) - { - WallpaperPreviewGrid.RowDefinitions.Add( - new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); - } - - for (var col = 0; col < gridMetrics.ColumnCount; col++) - { - WallpaperPreviewGrid.ColumnDefinitions.Add( - new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); - } - - PlaceStatusBarComponent( - WallpaperPreviewTopStatusBarHost, - column: 0, - requestedColumnSpan: gridMetrics.ColumnCount, - totalColumns: gridMetrics.ColumnCount); - - var taskbarRow = gridMetrics.RowCount - 1; - Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow); - Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0); - Grid.SetRowSpan(WallpaperPreviewBottomTaskbarContainer, 1); - Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); - - ApplyTopStatusComponentVisibility(); - ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); - ApplyPreviewWidgetSizing(gridMetrics.CellSize); - ApplyStatusBarComponentSpacingForPanel(WallpaperPreviewTopStatusComponentsPanel, gridMetrics.CellSize); - } - finally - { - _isUpdatingWallpaperPreviewLayout = false; - } - } - - private void ApplyPreviewWidgetSizing(double cellSize) - { - var previewTaskbarCell = Math.Clamp(cellSize * 0.74, 10, 28); - var previewTextSize = Math.Clamp(previewTaskbarCell * 0.38, 7, 14); - var previewIconSize = Math.Clamp(previewTaskbarCell * 0.46, 8, 16); - var previewInset = Math.Clamp(previewTaskbarCell * 0.20, 2, 6); - var previewContentSpacing = Math.Clamp(previewTaskbarCell * 0.20, 2, 6); - - // Match desktop behavior: special bars fill their preview row. - WallpaperPreviewTopStatusBarHost.Margin = new Thickness(0); - WallpaperPreviewTopStatusBarHost.Padding = new Thickness(0); - - WallpaperPreviewBottomTaskbarContainer.Margin = new Thickness(0); - WallpaperPreviewBottomTaskbarContainer.CornerRadius = new CornerRadius(Math.Clamp(cellSize * 0.45, 6, 14)); - WallpaperPreviewBottomTaskbarContainer.Padding = new Thickness(previewInset); - - WallpaperPreviewClockWidget.ApplyCellSize(cellSize); - WallpaperPreviewBackButtonTextBlock.FontSize = previewTextSize; - WallpaperPreviewComponentLibraryTextBlock.FontSize = previewTextSize; - WallpaperPreviewBackButtonVisual.Spacing = previewContentSpacing; - WallpaperPreviewComponentLibraryVisual.Spacing = previewContentSpacing; - - WallpaperPreviewBackButtonVisual.MinHeight = previewTaskbarCell; - WallpaperPreviewBackButtonVisual.MinWidth = Math.Clamp(cellSize * 2.1, 30, 120); - WallpaperPreviewComponentLibraryVisual.MinHeight = previewTaskbarCell; - WallpaperPreviewComponentLibraryVisual.MinWidth = Math.Clamp(cellSize * 2.0, 28, 110); - - WallpaperPreviewSettingsButtonIcon.Width = previewIconSize; - WallpaperPreviewSettingsButtonIcon.Height = previewIconSize; - } - private void OnMinimizeClick(object? sender, RoutedEventArgs e) { WindowState = WindowState.Minimized; diff --git a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml index 6058248..9a10cf9 100644 --- a/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml +++ b/LanMountainDesktop/Views/SettingsPages/WallpaperSettingsPage.axaml @@ -22,9 +22,26 @@ BoxShadow="0 12 32 #50000000"> - + + + + + + + + + @@ -90,7 +107,7 @@ @@ -130,6 +147,12 @@ + + diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml b/LanMountainDesktop/Views/SettingsWindow.axaml index 6af7b1e..65fcddc 100644 --- a/LanMountainDesktop/Views/SettingsWindow.axaml +++ b/LanMountainDesktop/Views/SettingsWindow.axaml @@ -14,7 +14,6 @@ SystemDecorations="BorderOnly" FontFamily="{DynamicResource AppFontFamily}" Background="Transparent" - Icon="/Assets/avalonia-logo.ico" Title="{Binding Title}"> diff --git a/LanMountainDesktop/Views/SettingsWindow.axaml.cs b/LanMountainDesktop/Views/SettingsWindow.axaml.cs index f7ecadf..2687b7a 100644 --- a/LanMountainDesktop/Views/SettingsWindow.axaml.cs +++ b/LanMountainDesktop/Views/SettingsWindow.axaml.cs @@ -30,6 +30,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext private readonly ISettingsPageRegistry _pageRegistry; private readonly IHostApplicationLifecycle _hostApplicationLifecycle; + private readonly IAppLogoService _appLogoService = HostAppLogoProvider.GetOrCreate(); private readonly Dictionary _cachedPages = new(StringComparer.OrdinalIgnoreCase); private readonly bool _useSystemChrome; private bool _isResponsiveRefreshPending; @@ -55,6 +56,7 @@ public partial class SettingsWindow : Window, ISettingsPageHostContext _hostApplicationLifecycle = hostApplicationLifecycle; DataContext = ViewModel; InitializeComponent(); + Icon = _appLogoService.CreateWindowIcon(); ApplyChromeMode(useSystemChrome); if (RootNavigationView is not null) diff --git a/LanMountainDesktop/installer/LanMountainDesktop.iss b/LanMountainDesktop/installer/LanMountainDesktop.iss index 3c5b26f..0d21918 100644 --- a/LanMountainDesktop/installer/LanMountainDesktop.iss +++ b/LanMountainDesktop/installer/LanMountainDesktop.iss @@ -31,7 +31,7 @@ UsePreviousAppDir=no ShowLanguageDialog=yes UsePreviousLanguage=no LanguageDetectionMethod=uilanguage -DefaultGroupName={#MyAppName} +DefaultGroupName={cm:AppShortcutName} UninstallDisplayIcon={app}\{#MyAppExeName} OutputDir={#MyOutputDir} OutputBaseFilename={#MyAppName}-Setup-{#MyAppVersion}-{#MyAppArch} @@ -62,6 +62,8 @@ Name: "chinesesimplified"; MessagesFile: "{#SourcePath}\ChineseSimplified.isl" [CustomMessages] english.StartupTaskDescription=Launch LanMountainDesktop when you sign in to Windows chinesesimplified.StartupTaskDescription=登录 Windows 时启动 LanMountainDesktop +english.AppShortcutName=LanMountainDesktop +chinesesimplified.AppShortcutName=阑山桌面 english.WebView2MissingMessage=Microsoft Edge WebView2 Runtime is required for the browser component. chinesesimplified.WebView2MissingMessage=浏览器组件需要 Microsoft Edge WebView2 Runtime。 english.WebView2MissingAction=Click "Yes" to open the official download page. Install it first, then run this installer again. @@ -111,8 +113,8 @@ Type: files; Name: "{app}\LanMontainDesktop.pdb" Source: "{#PublishDir}\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] -Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" -Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon +Name: "{autoprograms}\{cm:AppShortcutName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{autodesktop}\{cm:AppShortcutName}"; Filename: "{app}\{#MyAppExeName}"; Tasks: desktopicon [Registry] Root: HKA; Subkey: "Software\Microsoft\Windows\CurrentVersion\Run"; ValueType: string; ValueName: "{#MyAppName}"; ValueData: """{app}\{#MyAppExeName}"""; Tasks: startup; Flags: uninsdeletevalue