重构了设置系统。解决了大量的bug,正式添加了图标。引入了遥测的同意与许可(暂无实际功能)
This commit is contained in:
lincube
2026-03-15 21:27:48 +08:00
parent f83c6ede1d
commit 557b79e8c0
19 changed files with 1143 additions and 869 deletions

View File

@@ -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();
}
}
}

View File

@@ -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,
[],

View File

@@ -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<string> EnumerateAvatarCandidates()
{
var seen = new HashSet<string>(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<string> 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();
}
}
}

View File

@@ -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;
}
}