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 0000000..aa1524d Binary files /dev/null and b/LanMountainDesktop/Assets/logo_nightly.ico differ diff --git a/LanMountainDesktop/Assets/logo_nightly.png b/LanMountainDesktop/Assets/logo_nightly.png new file mode 100644 index 0000000..c89feab Binary files /dev/null and b/LanMountainDesktop/Assets/logo_nightly.png differ 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