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 @@
+
+
+
+
+
+
+
+
+

+
+
+
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