This commit is contained in:
lincube
2026-03-01 00:34:07 +08:00
parent 473a84e47b
commit f0e44c0f87
22 changed files with 3388 additions and 697 deletions

View File

@@ -3,4 +3,5 @@ namespace LanMontainDesktop.ComponentSystem;
public static class BuiltInComponentIds public static class BuiltInComponentIds
{ {
public const string Clock = "Clock"; public const string Clock = "Clock";
public const string Blank2x4 = "Blank2x4";
} }

View File

@@ -29,6 +29,15 @@ public sealed class ComponentRegistry
MinWidthCells: 1, MinWidthCells: 1,
MinHeightCells: 1, MinHeightCells: 1,
AllowStatusBarPlacement: true, AllowStatusBarPlacement: true,
AllowDesktopPlacement: true),
new DesktopComponentDefinition(
BuiltInComponentIds.Blank2x4,
"Blank 2x4",
"Rectangle",
"Layout",
MinWidthCells: 2,
MinHeightCells: 4,
AllowStatusBarPlacement: false,
AllowDesktopPlacement: true) AllowDesktopPlacement: true)
}; };

View File

@@ -28,7 +28,9 @@
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" /> <PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="FluentAvaloniaUI" Version="2.5.0" /> <PackageReference Include="FluentAvaloniaUI" Version="2.5.0" />
<PackageReference Include="FluentIcons.Avalonia" Version="2.0.319" /> <PackageReference Include="FluentIcons.Avalonia" Version="2.0.319" />
<PackageReference Include="FluentIcons.Avalonia.Fluent" Version="2.0.319" />
<PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" /> <PackageReference Include="LibVLCSharp.Avalonia" Version="3.9.5" />
<PackageReference Include="System.Drawing.Common" Version="10.0.0" />
<PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" /> <PackageReference Include="VideoLAN.LibVLC.Windows" Version="3.0.23" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@@ -74,9 +74,16 @@
"filepicker.video_files": "Video files", "filepicker.video_files": "Video files",
"common.day": "Day", "common.day": "Day",
"common.night": "Night", "common.night": "Night",
"common.back": "Back",
"common.close": "Close", "common.close": "Close",
"common.recommended": "Recommended", "common.recommended": "Recommended",
"common.monet": "Monet", "common.monet": "Monet",
"desktop.page_index_format": "Desktop {0}",
"launcher.title": "App Launcher",
"launcher.subtitle": "Apps and folders from Windows Start Menu",
"launcher.empty": "No Start Menu entries found.",
"launcher.empty_folder": "This folder is empty.",
"launcher.folder_items_format": "{0} apps",
"button.component_library": "Component Library", "button.component_library": "Component Library",
"tooltip.component_library": "Component Library", "tooltip.component_library": "Component Library",
"component_library.title": "Component Library", "component_library.title": "Component Library",

View File

@@ -12,7 +12,7 @@
"settings.nav.status_bar": "状态栏", "settings.nav.status_bar": "状态栏",
"settings.nav.region": "地区", "settings.nav.region": "地区",
"settings.wallpaper.title": "壁纸", "settings.wallpaper.title": "壁纸",
"settings.wallpaper.description": "选择图片或视频后可立设为应用窗口壁纸。", "settings.wallpaper.description": "选择图片或视频后可立设为应用窗口壁纸。",
"settings.wallpaper.current_label": "当前壁纸", "settings.wallpaper.current_label": "当前壁纸",
"settings.wallpaper.placement_label": "显示方式", "settings.wallpaper.placement_label": "显示方式",
"settings.wallpaper.pick_button": "浏览文件", "settings.wallpaper.pick_button": "浏览文件",
@@ -28,10 +28,10 @@
"settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。", "settings.wallpaper.video_mode": "视频壁纸使用自动填充模式。",
"settings.wallpaper.cleared": "背景已恢复为纯色。", "settings.wallpaper.cleared": "背景已恢复为纯色。",
"settings.wallpaper.default_status": "当前使用纯色背景。", "settings.wallpaper.default_status": "当前使用纯色背景。",
"settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,使用纯色背景。", "settings.wallpaper.saved_not_found": "未找到已保存的壁纸文件,使用纯色背景。",
"settings.wallpaper.restored": "已恢复保存的壁纸。", "settings.wallpaper.restored": "已恢复保存的壁纸。",
"settings.wallpaper.video_restored": "已恢复保存的视频壁纸。", "settings.wallpaper.video_restored": "已恢复保存的视频壁纸。",
"settings.wallpaper.restore_failed": "恢复已保存壁纸失败,使用纯色背景。", "settings.wallpaper.restore_failed": "恢复已保存壁纸失败,使用纯色背景。",
"settings.wallpaper.video_not_found": "未找到视频壁纸文件。", "settings.wallpaper.video_not_found": "未找到视频壁纸文件。",
"settings.wallpaper.video_player_unavailable": "视频播放器不可用。", "settings.wallpaper.video_player_unavailable": "视频播放器不可用。",
"settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}", "settings.wallpaper.video_play_failed_format": "播放视频壁纸失败:{0}",
@@ -54,8 +54,8 @@
"settings.color.monet_refreshed": "莫奈色已刷新。", "settings.color.monet_refreshed": "莫奈色已刷新。",
"settings.color.theme_ready_format": "主题色已就绪:{0}。", "settings.color.theme_ready_format": "主题色已就绪:{0}。",
"settings.color.theme_applied_format": "{0}主题色已应用:{1}。", "settings.color.theme_applied_format": "{0}主题色已应用:{1}。",
"settings.color.theme_updated_wallpaper": "壁纸更新,莫奈色已刷新。", "settings.color.theme_updated_wallpaper": "壁纸更新,莫奈色已刷新。",
"settings.color.theme_updated_video": "视频壁纸更新,主题色已刷新。", "settings.color.theme_updated_video": "视频壁纸更新,主题色已刷新。",
"settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。", "settings.color.theme_cleared_wallpaper": "壁纸已清除,莫奈色已刷新。",
"settings.status_bar.title": "状态栏", "settings.status_bar.title": "状态栏",
"settings.status_bar.description": "选择顶部状态栏显示的组件。", "settings.status_bar.description": "选择顶部状态栏显示的组件。",
@@ -74,8 +74,20 @@
"filepicker.video_files": "视频文件", "filepicker.video_files": "视频文件",
"common.day": "日间", "common.day": "日间",
"common.night": "夜间", "common.night": "夜间",
"common.back": "返回",
"common.close": "关闭",
"common.recommended": "推荐", "common.recommended": "推荐",
"common.monet": "莫奈", "common.monet": "莫奈",
"desktop.page_index_format": "桌面 {0}",
"launcher.title": "应用启动台",
"launcher.subtitle": "按 Windows 开始菜单结构显示所有应用与文件夹",
"launcher.empty": "未找到开始菜单条目。",
"launcher.empty_folder": "此文件夹为空。",
"launcher.folder_items_format": "{0} 个应用",
"button.component_library": "组件库",
"tooltip.component_library": "组件库",
"component_library.title": "组件库",
"component_library.empty": "暂无组件,后续会在这里显示。",
"placement.fill": "填充", "placement.fill": "填充",
"placement.fit": "适应", "placement.fit": "适应",
"placement.stretch": "拉伸", "placement.stretch": "拉伸",

View File

@@ -29,4 +29,8 @@ public sealed class AppSettingsSnapshot
public bool EnableDynamicTaskbarActions { get; set; } = false; public bool EnableDynamicTaskbarActions { get; set; } = false;
public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle"; public string TaskbarLayoutMode { get; set; } = "BottomFullRowMacStyle";
public int DesktopPageCount { get; set; } = 1;
public int CurrentDesktopSurfaceIndex { get; set; } = 0;
} }

View File

@@ -0,0 +1,12 @@
namespace LanMontainDesktop.Models;
public sealed class StartMenuAppEntry
{
public required string DisplayName { get; init; }
public required string FilePath { get; init; }
public required string RelativePath { get; init; }
public byte[]? IconPngBytes { get; init; }
}

View File

@@ -0,0 +1,24 @@
using System.Collections.Generic;
using System.Linq;
namespace LanMontainDesktop.Models;
public sealed class StartMenuFolderNode
{
public StartMenuFolderNode(string name, string relativePath)
{
Name = name;
RelativePath = relativePath;
}
public string Name { get; }
public string RelativePath { get; }
public List<StartMenuFolderNode> Folders { get; } = [];
public List<StartMenuAppEntry> Apps { get; } = [];
public int TotalAppCount => Apps.Count + Folders.Sum(folder => folder.TotalAppCount);
}

View File

@@ -41,17 +41,17 @@ public static class GlassEffectService
// 面板颜色 - 使用 Mica 材质 // 面板颜色 - 使用 Mica 材质
resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush( resources["AdaptiveGlassPanelBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF2, Color.FromArgb(context.IsNightMode ? (byte)0xF0 : (byte)0xF8,
micaBackground.R, micaBackground.G, micaBackground.B)); micaBackground.R, micaBackground.G, micaBackground.B));
resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush( resources["AdaptiveGlassPanelBorderBrush"] = new SolidColorBrush(
Color.FromArgb(0x1F, neutralElevated.R, neutralElevated.G, neutralElevated.B)); Color.FromArgb(0x1F, neutralElevated.R, neutralElevated.G, neutralElevated.B));
resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush( resources["AdaptiveGlassStrongBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF5, Color.FromArgb(context.IsNightMode ? (byte)0xF4 : (byte)0xFB,
micaElevated.R, micaElevated.G, micaElevated.B)); micaElevated.R, micaElevated.G, micaElevated.B));
resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush( resources["AdaptiveGlassStrongBorderBrush"] = new SolidColorBrush(
Color.FromArgb(0x29, neutralElevated.R, neutralElevated.G, neutralElevated.B)); Color.FromArgb(0x29, neutralElevated.R, neutralElevated.G, neutralElevated.B));
resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush( resources["AdaptiveGlassOverlayBackgroundBrush"] = new SolidColorBrush(
Color.FromArgb(context.IsNightMode ? (byte)0xCC : (byte)0xE6, Color.FromArgb(context.IsNightMode ? (byte)0xE6 : (byte)0xF2,
micaBackground.R, micaBackground.G, micaBackground.B)); micaBackground.R, micaBackground.G, micaBackground.B));
// 模糊半径Mica 不需要强模糊) // 模糊半径Mica 不需要强模糊)
@@ -60,9 +60,9 @@ public static class GlassEffectService
resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 30.0 : 40.0; resources["AdaptiveGlassOverlayBlurRadius"] = context.IsNightMode ? 30.0 : 40.0;
// 不透明度Mica 材质接近不透明) // 不透明度Mica 材质接近不透明)
resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.95 : 0.98; resources["AdaptiveGlassPanelOpacity"] = context.IsNightMode ? 0.99 : 1.0;
resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 0.97 : 0.99; resources["AdaptiveGlassStrongOpacity"] = context.IsNightMode ? 1.0 : 1.0;
resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.85 : 0.92; resources["AdaptiveGlassOverlayOpacity"] = context.IsNightMode ? 0.94 : 0.97;
resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.01 : 0.008; resources["AdaptiveGlassNoiseOpacity"] = context.IsNightMode ? 0.01 : 0.008;
} }
} }

View File

@@ -0,0 +1,475 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
using System.Xml.Linq;
namespace LanMontainDesktop.Services;
[SupportedOSPlatform("windows")]
internal static class UwpManifestIconResolver
{
private const int ErrorSuccess = 0;
private const int ErrorInsufficientBuffer = 122;
private static readonly Regex ScaleRegex =
new(@"scale-(?<n>\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly Regex TargetSizeRegex =
new(@"targetsize-(?<n>\d+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
private static readonly string[] ManifestLogoAttributePriority =
[
"Square44x44Logo",
"Square150x150Logo",
"Square310x310Logo",
"Square71x71Logo",
"Wide310x150Logo",
"Logo",
"SmallLogo"
];
private static readonly Dictionary<string, byte[]?> IconCache = new(StringComparer.OrdinalIgnoreCase);
private static readonly object CacheLock = new();
public static bool TryGetIconPngBytesFromAumid(string aumid, out byte[]? pngBytes)
{
pngBytes = null;
if (string.IsNullOrWhiteSpace(aumid))
{
return false;
}
lock (CacheLock)
{
if (IconCache.TryGetValue(aumid, out var cached))
{
pngBytes = cached;
return cached is not null;
}
}
if (!TrySplitAumid(aumid, out var packageFamilyName, out var appId))
{
return false;
}
foreach (var installLocation in GetPackageInstallLocations(packageFamilyName))
{
if (TryExtractIconFromManifestInstallLocation(installLocation, appId, out pngBytes))
{
lock (CacheLock)
{
IconCache[aumid] = pngBytes;
}
return true;
}
}
lock (CacheLock)
{
IconCache[aumid] = null;
}
return false;
}
private static bool TrySplitAumid(string aumid, out string packageFamilyName, out string appId)
{
packageFamilyName = string.Empty;
appId = string.Empty;
var separatorIndex = aumid.IndexOf('!');
if (separatorIndex <= 0 || separatorIndex >= aumid.Length - 1)
{
return false;
}
packageFamilyName = aumid[..separatorIndex].Trim();
appId = aumid[(separatorIndex + 1)..].Trim();
return !string.IsNullOrWhiteSpace(packageFamilyName) && !string.IsNullOrWhiteSpace(appId);
}
private static IReadOnlyList<string> GetPackageInstallLocations(string packageFamilyName)
{
var fromApi = GetPackageInstallLocationsByFamilyApi(packageFamilyName);
return fromApi.Count > 0
? fromApi
: GetPackageInstallLocationsByDirectoryScan(packageFamilyName);
}
private static IReadOnlyList<string> GetPackageInstallLocationsByFamilyApi(string packageFamilyName)
{
var results = new List<(string PackageFullName, string InstallLocation)>();
uint packageCount = 0;
uint bufferLength = 0;
var firstCall = GetPackagesByPackageFamily(
packageFamilyName,
ref packageCount,
null,
ref bufferLength,
null);
if (firstCall != ErrorInsufficientBuffer && firstCall != ErrorSuccess)
{
return [];
}
if (packageCount == 0)
{
return [];
}
var packageFullNamePointers = new IntPtr[packageCount];
var packageNamesBuffer = new char[Math.Max(1, (int)bufferLength)];
var secondCall = GetPackagesByPackageFamily(
packageFamilyName,
ref packageCount,
packageFullNamePointers,
ref bufferLength,
packageNamesBuffer);
if (secondCall != ErrorSuccess)
{
return [];
}
for (var i = 0; i < packageCount; i++)
{
var packageFullName = Marshal.PtrToStringUni(packageFullNamePointers[i]);
if (string.IsNullOrWhiteSpace(packageFullName))
{
continue;
}
uint pathLength = 0;
var getPathFirst = GetPackagePathByFullName(packageFullName, ref pathLength, null);
if (getPathFirst != ErrorInsufficientBuffer || pathLength == 0)
{
continue;
}
var pathBuilder = new StringBuilder((int)pathLength);
var getPathSecond = GetPackagePathByFullName(packageFullName, ref pathLength, pathBuilder);
if (getPathSecond != ErrorSuccess)
{
continue;
}
var installPath = pathBuilder.ToString().TrimEnd('\0').Trim();
if (string.IsNullOrWhiteSpace(installPath) || !Directory.Exists(installPath))
{
continue;
}
results.Add((packageFullName, installPath));
}
return results
.OrderByDescending(item => ParsePackageVersion(item.PackageFullName))
.Select(item => item.InstallLocation)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static IReadOnlyList<string> GetPackageInstallLocationsByDirectoryScan(string packageFamilyName)
{
if (!TrySplitPackageFamilyName(packageFamilyName, out var packageName, out var publisherId))
{
return [];
}
var windowsAppsDirectory = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
"WindowsApps");
if (!Directory.Exists(windowsAppsDirectory))
{
return [];
}
try
{
var directories = Directory
.EnumerateDirectories(windowsAppsDirectory)
.Where(directoryPath =>
{
var directoryName = Path.GetFileName(directoryPath);
if (string.IsNullOrWhiteSpace(directoryName))
{
return false;
}
return directoryName.StartsWith(packageName + "_", StringComparison.OrdinalIgnoreCase) &&
directoryName.Contains("__" + publisherId, StringComparison.OrdinalIgnoreCase);
})
.OrderByDescending(directoryPath => ParsePackageVersion(Path.GetFileName(directoryPath)))
.ToList();
return directories;
}
catch
{
return [];
}
}
private static bool TrySplitPackageFamilyName(string packageFamilyName, out string packageName, out string publisherId)
{
packageName = string.Empty;
publisherId = string.Empty;
if (string.IsNullOrWhiteSpace(packageFamilyName))
{
return false;
}
var separatorIndex = packageFamilyName.LastIndexOf('_');
if (separatorIndex <= 0 || separatorIndex >= packageFamilyName.Length - 1)
{
return false;
}
packageName = packageFamilyName[..separatorIndex].Trim();
publisherId = packageFamilyName[(separatorIndex + 1)..].Trim();
return !string.IsNullOrWhiteSpace(packageName) && !string.IsNullOrWhiteSpace(publisherId);
}
private static Version ParsePackageVersion(string? packageIdentity)
{
if (string.IsNullOrWhiteSpace(packageIdentity))
{
return new Version(0, 0);
}
var identity = packageIdentity.Trim();
var firstUnderscore = identity.IndexOf('_');
if (firstUnderscore < 0 || firstUnderscore >= identity.Length - 1)
{
return new Version(0, 0);
}
var secondUnderscore = identity.IndexOf('_', firstUnderscore + 1);
if (secondUnderscore < 0)
{
return new Version(0, 0);
}
var versionText = identity[(firstUnderscore + 1)..secondUnderscore];
return Version.TryParse(versionText, out var version)
? version
: new Version(0, 0);
}
private static bool TryExtractIconFromManifestInstallLocation(string installLocation, string appId, out byte[]? pngBytes)
{
pngBytes = null;
var manifestPath = Path.Combine(installLocation, "AppxManifest.xml");
if (!File.Exists(manifestPath))
{
return false;
}
XDocument document;
try
{
document = XDocument.Load(manifestPath);
}
catch
{
return false;
}
var applicationNodes = document
.Descendants()
.Where(node => string.Equals(node.Name.LocalName, "Application", StringComparison.OrdinalIgnoreCase))
.ToList();
if (applicationNodes.Count == 0)
{
return false;
}
var applicationNode = applicationNodes.FirstOrDefault(node =>
string.Equals(
node.Attributes().FirstOrDefault(attr => string.Equals(attr.Name.LocalName, "Id", StringComparison.OrdinalIgnoreCase))?.Value,
appId,
StringComparison.OrdinalIgnoreCase)) ?? applicationNodes.First();
var logoCandidates = new List<string>();
CollectManifestLogoCandidates(applicationNode, logoCandidates);
foreach (var rawLogoPath in logoCandidates.Distinct(StringComparer.OrdinalIgnoreCase))
{
foreach (var candidateFilePath in EnumerateManifestLogoFiles(installLocation, rawLogoPath))
{
if (TryConvertImageFileToPngBytes(candidateFilePath, out pngBytes))
{
return true;
}
}
}
return false;
}
private static void CollectManifestLogoCandidates(XElement applicationNode, List<string> output)
{
foreach (var node in applicationNode.DescendantsAndSelf())
{
var localName = node.Name.LocalName;
if (!string.Equals(localName, "VisualElements", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(localName, "DefaultTile", StringComparison.OrdinalIgnoreCase))
{
continue;
}
foreach (var attributeName in ManifestLogoAttributePriority)
{
var attribute = node
.Attributes()
.FirstOrDefault(attr => string.Equals(attr.Name.LocalName, attributeName, StringComparison.OrdinalIgnoreCase));
if (attribute is not null && !string.IsNullOrWhiteSpace(attribute.Value))
{
output.Add(attribute.Value.Trim());
}
}
}
}
private static IEnumerable<string> EnumerateManifestLogoFiles(string installLocation, string rawLogoPath)
{
var normalizedAssetPath = NormalizeManifestAssetPath(rawLogoPath);
if (string.IsNullOrWhiteSpace(normalizedAssetPath))
{
return [];
}
var baseCandidatePath = Path.GetFullPath(Path.Combine(installLocation, normalizedAssetPath));
var files = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (File.Exists(baseCandidatePath))
{
files.Add(baseCandidatePath);
}
var directoryPath = Path.GetDirectoryName(baseCandidatePath);
if (string.IsNullOrWhiteSpace(directoryPath) || !Directory.Exists(directoryPath))
{
return files;
}
var baseName = Path.GetFileNameWithoutExtension(baseCandidatePath);
var extension = Path.GetExtension(baseCandidatePath);
var searchPattern = string.IsNullOrWhiteSpace(extension)
? baseName + ".*"
: baseName + "*" + extension;
try
{
foreach (var filePath in Directory.EnumerateFiles(directoryPath, searchPattern, SearchOption.TopDirectoryOnly))
{
files.Add(filePath);
}
}
catch
{
// ignore inaccessible folders
}
return files
.OrderByDescending(ScoreManifestIconCandidate)
.ThenBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToList();
}
private static string NormalizeManifestAssetPath(string rawValue)
{
if (string.IsNullOrWhiteSpace(rawValue))
{
return string.Empty;
}
var cleaned = rawValue.Trim().Trim('"');
if (cleaned.StartsWith("ms-resource:", StringComparison.OrdinalIgnoreCase))
{
return string.Empty;
}
if (cleaned.StartsWith("ms-appx:///", StringComparison.OrdinalIgnoreCase))
{
cleaned = cleaned["ms-appx:///".Length..];
}
cleaned = cleaned
.Replace('/', Path.DirectorySeparatorChar)
.Replace('\\', Path.DirectorySeparatorChar)
.TrimStart(Path.DirectorySeparatorChar);
return cleaned;
}
private static int ScoreManifestIconCandidate(string filePath)
{
var score = 0;
var fileName = Path.GetFileName(filePath);
var targetSizeMatch = TargetSizeRegex.Match(fileName);
if (targetSizeMatch.Success && int.TryParse(targetSizeMatch.Groups["n"].Value, out var targetSize))
{
score += targetSize * 100;
}
var scaleMatch = ScaleRegex.Match(fileName);
if (scaleMatch.Success && int.TryParse(scaleMatch.Groups["n"].Value, out var scale))
{
score += scale * 10;
}
if (fileName.Contains("altform-unplated", StringComparison.OrdinalIgnoreCase))
{
score += 300;
}
if (Path.GetExtension(fileName).Equals(".png", StringComparison.OrdinalIgnoreCase))
{
score += 80;
}
return score;
}
private static bool TryConvertImageFileToPngBytes(string filePath, out byte[]? pngBytes)
{
pngBytes = null;
try
{
using var image = Image.FromFile(filePath);
using var stream = new MemoryStream();
image.Save(stream, ImageFormat.Png);
pngBytes = stream.ToArray();
return true;
}
catch
{
return false;
}
}
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPackagesByPackageFamily(
string packageFamilyName,
ref uint count,
IntPtr[]? packageFullNames,
ref uint bufferLength,
char[]? buffer);
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetPackagePathByFullName(
string packageFullName,
ref uint pathLength,
StringBuilder? path);
}

View File

@@ -0,0 +1,859 @@
using System;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Runtime.Versioning;
using System.Text;
using System.Text.RegularExpressions;
namespace LanMontainDesktop.Services;
[SupportedOSPlatform("windows")]
internal static class WindowsIconService
{
private const int HighResolutionIconSize = 256;
private const int MaxShellPath = 1024;
private const int StgmRead = 0x00000000;
private const uint SiigbfBiggerSizeOk = 0x00000001;
private const uint SiigbfIconOnly = 0x00000004;
private const uint ShgfiIcon = 0x00000100;
private const uint ShgfiLargeIcon = 0x00000000;
private const uint ShgfiUseFileAttributes = 0x00000010;
private const uint FileAttributeNormal = 0x00000080;
private const uint FileAttributeDirectory = 0x00000010;
private const uint CoinitApartmentThreaded = 0x2;
private const int SOk = 0;
private const int SFalse = 1;
private const int RpcEChangedMode = unchecked((int)0x80010106);
private static readonly Guid CLSID_ShellLink = new("00021401-0000-0000-C000-000000000046");
private static readonly Guid IID_IShellItemImageFactory = new("BCC18B79-BA16-442F-80C4-8A59C30C463B");
private static readonly Regex AumidRegex =
new(@"shell:AppsFolder\\(?<aumid>[^\s""]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
public static byte[]? TryGetIconPngBytes(string filePath)
{
if (!OperatingSystem.IsWindows() || string.IsNullOrWhiteSpace(filePath))
{
return null;
}
var normalizedEntryPath = Path.GetFullPath(filePath);
if (!File.Exists(normalizedEntryPath))
{
return null;
}
try
{
var extension = Path.GetExtension(normalizedEntryPath);
if (extension.Equals(".lnk", StringComparison.OrdinalIgnoreCase))
{
if (TryReadLnkIconLocation(normalizedEntryPath, out var iconLocation, out var iconIndex) &&
TryResolveIconPath(iconLocation, normalizedEntryPath, out var resolvedIconPath) &&
TryExtractIconFromResourceFile(resolvedIconPath, iconIndex, out var pngBytesFromLnkIconLocation))
{
return pngBytesFromLnkIconLocation;
}
if (TryReadLnkArguments(normalizedEntryPath, out var arguments) &&
TryParseAumidFromArguments(arguments, out var aumid))
{
var appsFolderPath = $"shell:AppsFolder\\{aumid}";
if (TryExtractIconWithShellItemImageFactory(appsFolderPath, out var pngBytesFromAppsFolder))
{
return pngBytesFromAppsFolder;
}
if (UwpManifestIconResolver.TryGetIconPngBytesFromAumid(aumid, out var pngBytesFromManifest))
{
return pngBytesFromManifest;
}
}
if (TryReadLnkTargetPath(normalizedEntryPath, out var targetPath) &&
TryExtractIconFromResourceFile(targetPath, 0, out var pngBytesFromLnkTarget))
{
return pngBytesFromLnkTarget;
}
}
else if (extension.Equals(".url", StringComparison.OrdinalIgnoreCase))
{
if (TryReadUrlIconLocation(normalizedEntryPath, out var iconFile, out var iconIndex) &&
TryResolveIconPath(iconFile, normalizedEntryPath, out var resolvedIconPath) &&
TryExtractIconFromResourceFile(resolvedIconPath, iconIndex, out var pngBytesFromUrlIconLocation))
{
return pngBytesFromUrlIconLocation;
}
}
if (TryExtractIconWithShellItemImageFactory(normalizedEntryPath, out var pngBytesFromShellItem))
{
return pngBytesFromShellItem;
}
if (TryExtractIconWithShGetFileInfo(normalizedEntryPath, out var pngBytesFromShGetFileInfo))
{
return pngBytesFromShGetFileInfo;
}
return null;
}
catch
{
return null;
}
}
public static byte[]? TryGetSystemFolderIconPngBytes()
{
if (!OperatingSystem.IsWindows())
{
return null;
}
// Prefer the HICON-based path first to preserve alpha better for folder glyphs.
if (TryExtractFolderIconWithShGetFileInfo(out var shGetFolderIcon) &&
shGetFolderIcon is not null)
{
return shGetFolderIcon;
}
var isWin11 = OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000);
var preferredProbePaths = isWin11
? new[]
{
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile),
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
Environment.SystemDirectory
}
: new[]
{
Environment.SystemDirectory,
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)
};
foreach (var probePath in preferredProbePaths)
{
if (string.IsNullOrWhiteSpace(probePath) || !Directory.Exists(probePath))
{
continue;
}
if (TryExtractIconWithShellItemImageFactory(probePath, out var shellFolderIcon))
{
return shellFolderIcon;
}
}
return null;
}
private static bool TryParseAumidFromArguments(string arguments, out string aumid)
{
aumid = string.Empty;
if (string.IsNullOrWhiteSpace(arguments))
{
return false;
}
var match = AumidRegex.Match(arguments);
if (!match.Success)
{
return false;
}
aumid = match.Groups["aumid"].Value.Trim().Trim('"');
return !string.IsNullOrWhiteSpace(aumid);
}
private static bool TryReadLnkIconLocation(string lnkFilePath, out string iconLocation, out int iconIndex)
{
iconLocation = string.Empty;
iconIndex = 0;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var iconPathBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetIconLocation(iconPathBuilder, iconPathBuilder.Capacity, out iconIndex) < 0)
{
return false;
}
iconLocation = iconPathBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(iconLocation);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryReadLnkTargetPath(string lnkFilePath, out string targetPath)
{
targetPath = string.Empty;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var targetPathBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetPath(targetPathBuilder, targetPathBuilder.Capacity, IntPtr.Zero, 0) < 0)
{
return false;
}
targetPath = targetPathBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(targetPath);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryReadLnkArguments(string lnkFilePath, out string arguments)
{
arguments = string.Empty;
if (!TryInitializeCom(out var shouldUninitialize))
{
return false;
}
try
{
if (!TryCreateShellLink(out var shellLink))
{
return false;
}
try
{
if (!TryLoadShellLink(shellLink, lnkFilePath))
{
return false;
}
var argumentsBuilder = new StringBuilder(MaxShellPath);
if (shellLink.GetArguments(argumentsBuilder, argumentsBuilder.Capacity) < 0)
{
return false;
}
arguments = argumentsBuilder.ToString().Trim();
return !string.IsNullOrWhiteSpace(arguments);
}
finally
{
Marshal.FinalReleaseComObject(shellLink);
}
}
catch
{
return false;
}
finally
{
UninitializeCom(shouldUninitialize);
}
}
private static bool TryCreateShellLink(out IShellLinkW shellLink)
{
shellLink = null!;
var shellLinkType = Type.GetTypeFromCLSID(CLSID_ShellLink);
if (shellLinkType is null)
{
return false;
}
shellLink = (IShellLinkW?)Activator.CreateInstance(shellLinkType)!;
return shellLink is not null;
}
private static bool TryLoadShellLink(IShellLinkW shellLink, string lnkFilePath)
{
if (shellLink is not IPersistFile persistFile)
{
return false;
}
try
{
persistFile.Load(lnkFilePath, StgmRead);
return true;
}
catch
{
return false;
}
}
private static bool TryReadUrlIconLocation(string urlFilePath, out string iconFile, out int iconIndex)
{
iconFile = string.Empty;
iconIndex = 0;
if (!File.Exists(urlFilePath))
{
return false;
}
try
{
foreach (var rawLine in File.ReadLines(urlFilePath))
{
var line = rawLine.Trim();
if (line.StartsWith("IconFile=", StringComparison.OrdinalIgnoreCase))
{
iconFile = line["IconFile=".Length..].Trim();
continue;
}
if (line.StartsWith("IconIndex=", StringComparison.OrdinalIgnoreCase) &&
int.TryParse(line["IconIndex=".Length..].Trim(), out var parsedIndex))
{
iconIndex = parsedIndex;
}
}
}
catch
{
return false;
}
if (string.IsNullOrWhiteSpace(iconFile))
{
return false;
}
if (TrySplitIconFileAndIndex(iconFile, out var splitIconFile, out var splitIndex))
{
iconFile = splitIconFile;
if (iconIndex == 0)
{
iconIndex = splitIndex;
}
}
return true;
}
private static bool TryResolveIconPath(string rawIconLocation, string shortcutPath, out string resolvedIconPath)
{
resolvedIconPath = string.Empty;
if (string.IsNullOrWhiteSpace(rawIconLocation))
{
return false;
}
var cleaned = rawIconLocation.Trim().Trim('"');
if (cleaned.StartsWith("@", StringComparison.Ordinal))
{
cleaned = cleaned[1..];
}
if (TrySplitIconFileAndIndex(cleaned, out var splitPath, out _))
{
cleaned = splitPath;
}
cleaned = Environment.ExpandEnvironmentVariables(cleaned);
if (cleaned.StartsWith("http://", StringComparison.OrdinalIgnoreCase) ||
cleaned.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
{
return false;
}
if (!Path.IsPathRooted(cleaned))
{
var shortcutDirectory = Path.GetDirectoryName(shortcutPath);
if (!string.IsNullOrWhiteSpace(shortcutDirectory))
{
var relativeResolved = Path.GetFullPath(Path.Combine(shortcutDirectory, cleaned));
if (File.Exists(relativeResolved))
{
resolvedIconPath = relativeResolved;
return true;
}
}
var systemResolved = Path.Combine(Environment.SystemDirectory, cleaned);
if (File.Exists(systemResolved))
{
resolvedIconPath = systemResolved;
return true;
}
}
if (File.Exists(cleaned))
{
resolvedIconPath = cleaned;
return true;
}
return false;
}
private static bool TrySplitIconFileAndIndex(string rawValue, out string iconFile, out int iconIndex)
{
iconFile = rawValue.Trim();
iconIndex = 0;
if (string.IsNullOrWhiteSpace(iconFile))
{
return false;
}
var commaIndex = iconFile.LastIndexOf(',');
if (commaIndex <= 0 || commaIndex >= iconFile.Length - 1)
{
return false;
}
var possibleIndex = iconFile[(commaIndex + 1)..].Trim();
if (!int.TryParse(possibleIndex, out iconIndex))
{
return false;
}
iconFile = iconFile[..commaIndex].Trim().Trim('"');
return !string.IsNullOrWhiteSpace(iconFile);
}
private static bool TryExtractIconFromResourceFile(string resourceFilePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
if (string.IsNullOrWhiteSpace(resourceFilePath) || !File.Exists(resourceFilePath))
{
return false;
}
if (TryExtractIconWithShDefExtractIcon(resourceFilePath, iconIndex, out pngBytes))
{
return true;
}
if (TryExtractIconWithPrivateExtractIcons(resourceFilePath, iconIndex, out pngBytes))
{
return true;
}
return TryExtractIconWithExtractIconEx(resourceFilePath, iconIndex, out pngBytes);
}
private static bool TryExtractIconWithShDefExtractIcon(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var requestedSize = MakeLong(HighResolutionIconSize, HighResolutionIconSize);
var hr = SHDefExtractIcon(filePath, iconIndex, 0, out var largeIcon, out _, (uint)requestedSize);
if (hr < 0 || largeIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(largeIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(largeIcon);
}
}
private static bool TryExtractIconWithPrivateExtractIcons(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var iconHandles = new IntPtr[1];
var iconIds = new uint[1];
var extracted = PrivateExtractIcons(
filePath,
iconIndex,
HighResolutionIconSize,
HighResolutionIconSize,
iconHandles,
iconIds,
1,
0);
if (extracted == 0 || extracted == 0xFFFFFFFF || iconHandles[0] == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(iconHandles[0]);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(iconHandles[0]);
}
}
private static bool TryExtractIconWithExtractIconEx(string filePath, int iconIndex, out byte[]? pngBytes)
{
pngBytes = null;
var largeIcons = new IntPtr[1];
var extracted = ExtractIconEx(filePath, iconIndex, largeIcons, null, 1);
if (extracted <= 0 || largeIcons[0] == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(largeIcons[0]);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(largeIcons[0]);
}
}
private static bool TryExtractIconWithShellItemImageFactory(string filePath, out byte[]? pngBytes)
{
pngBytes = null;
if (SHCreateItemFromParsingName(filePath, IntPtr.Zero, IID_IShellItemImageFactory, out var imageFactoryObject) < 0 ||
imageFactoryObject is null)
{
return false;
}
try
{
var imageFactory = (IShellItemImageFactory)imageFactoryObject;
var size = new SizeStruct { cx = HighResolutionIconSize, cy = HighResolutionIconSize };
var flags = SiigbfIconOnly | SiigbfBiggerSizeOk;
if (imageFactory.GetImage(size, flags, out var hBitmap) < 0 || hBitmap == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHbitmapToPngBytes(hBitmap);
return pngBytes is not null;
}
finally
{
_ = DeleteObject(hBitmap);
}
}
catch
{
return false;
}
finally
{
Marshal.FinalReleaseComObject(imageFactoryObject);
}
}
private static bool TryExtractIconWithShGetFileInfo(string filePath, out byte[]? pngBytes)
{
pngBytes = null;
if (SHGetFileInfo(
filePath,
FileAttributeNormal,
out var fileInfo,
(uint)Marshal.SizeOf<SHFILEINFO>(),
ShgfiIcon | ShgfiUseFileAttributes) == IntPtr.Zero ||
fileInfo.hIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(fileInfo.hIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(fileInfo.hIcon);
}
}
private static bool TryExtractFolderIconWithShGetFileInfo(out byte[]? pngBytes)
{
pngBytes = null;
if (SHGetFileInfo(
"folder",
FileAttributeDirectory,
out var fileInfo,
(uint)Marshal.SizeOf<SHFILEINFO>(),
ShgfiIcon | ShgfiLargeIcon | ShgfiUseFileAttributes) == IntPtr.Zero ||
fileInfo.hIcon == IntPtr.Zero)
{
return false;
}
try
{
pngBytes = ConvertHiconToPngBytes(fileInfo.hIcon);
return pngBytes is not null;
}
finally
{
_ = DestroyIcon(fileInfo.hIcon);
}
}
private static byte[]? ConvertHiconToPngBytes(IntPtr iconHandle)
{
if (iconHandle == IntPtr.Zero)
{
return null;
}
try
{
using var icon = Icon.FromHandle(iconHandle);
var width = Math.Max(16, icon.Width);
var height = Math.Max(16, icon.Height);
using var bitmap = new Bitmap(width, height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawIcon(icon, new Rectangle(0, 0, width, height));
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
catch
{
return null;
}
}
private static byte[]? ConvertHbitmapToPngBytes(IntPtr bitmapHandle)
{
if (bitmapHandle == IntPtr.Zero)
{
return null;
}
try
{
using var source = Image.FromHbitmap(bitmapHandle);
using var bitmap = new Bitmap(source.Width, source.Height, PixelFormat.Format32bppArgb);
using (var graphics = Graphics.FromImage(bitmap))
{
graphics.Clear(Color.Transparent);
graphics.CompositingMode = CompositingMode.SourceOver;
graphics.CompositingQuality = CompositingQuality.HighQuality;
graphics.InterpolationMode = InterpolationMode.HighQualityBicubic;
graphics.SmoothingMode = SmoothingMode.HighQuality;
graphics.PixelOffsetMode = PixelOffsetMode.HighQuality;
graphics.DrawImage(source, 0, 0, source.Width, source.Height);
}
using var stream = new MemoryStream();
bitmap.Save(stream, ImageFormat.Png);
return stream.ToArray();
}
catch
{
return null;
}
}
private static bool TryInitializeCom(out bool shouldUninitialize)
{
shouldUninitialize = false;
var result = CoInitializeEx(IntPtr.Zero, CoinitApartmentThreaded);
if (result is SOk or SFalse)
{
shouldUninitialize = true;
return true;
}
return result == RpcEChangedMode;
}
private static void UninitializeCom(bool shouldUninitialize)
{
if (shouldUninitialize)
{
CoUninitialize();
}
}
private static int MakeLong(int lowWord, int highWord)
{
return (highWord << 16) | (lowWord & 0xFFFF);
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
private struct SHFILEINFO
{
public IntPtr hIcon;
public int iIcon;
public uint dwAttributes;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string szDisplayName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 80)]
public string szTypeName;
}
[StructLayout(LayoutKind.Sequential)]
private struct SizeStruct
{
public int cx;
public int cy;
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("000214F9-0000-0000-C000-000000000046")]
private interface IShellLinkW
{
[PreserveSig]
int GetPath([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszFile, int cch, IntPtr pfd, uint fFlags);
[PreserveSig] int GetIDList(out IntPtr ppidl);
[PreserveSig] int SetIDList(IntPtr pidl);
[PreserveSig] int GetDescription([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszName, int cch);
[PreserveSig] int SetDescription([MarshalAs(UnmanagedType.LPWStr)] string pszName);
[PreserveSig] int GetWorkingDirectory([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszDir, int cch);
[PreserveSig] int SetWorkingDirectory([MarshalAs(UnmanagedType.LPWStr)] string pszDir);
[PreserveSig] int GetArguments([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszArgs, int cch);
[PreserveSig] int SetArguments([MarshalAs(UnmanagedType.LPWStr)] string pszArgs);
[PreserveSig] int GetHotkey(out short pwHotkey);
[PreserveSig] int SetHotkey(short wHotkey);
[PreserveSig] int GetShowCmd(out int piShowCmd);
[PreserveSig] int SetShowCmd(int iShowCmd);
[PreserveSig] int GetIconLocation([Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder pszIconPath, int cchIconPath, out int iIcon);
[PreserveSig] int SetIconLocation([MarshalAs(UnmanagedType.LPWStr)] string pszIconPath, int iIcon);
[PreserveSig] int SetRelativePath([MarshalAs(UnmanagedType.LPWStr)] string pszPathRel, uint dwReserved);
[PreserveSig] int Resolve(IntPtr hwnd, uint fFlags);
[PreserveSig] int SetPath([MarshalAs(UnmanagedType.LPWStr)] string pszFile);
}
[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid("BCC18B79-BA16-442F-80C4-8A59C30C463B")]
private interface IShellItemImageFactory
{
[PreserveSig]
int GetImage(SizeStruct size, uint flags, out IntPtr phbm);
}
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern IntPtr SHGetFileInfo(
string pszPath,
uint dwFileAttributes,
out SHFILEINFO psfi,
uint cbFileInfo,
uint uFlags);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHDefExtractIcon(
string pszIconFile,
int iIndex,
uint uFlags,
out IntPtr phiconLarge,
out IntPtr phiconSmall,
uint nIconSize);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern int SHCreateItemFromParsingName(
[MarshalAs(UnmanagedType.LPWStr)] string pszPath,
IntPtr pbc,
[MarshalAs(UnmanagedType.LPStruct)] Guid riid,
[MarshalAs(UnmanagedType.Interface)] out object ppv);
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern uint PrivateExtractIcons(
string szFileName,
int nIconIndex,
int cxIcon,
int cyIcon,
IntPtr[] phicon,
uint[] piconid,
uint nIcons,
uint flags);
[DllImport("shell32.dll", CharSet = CharSet.Unicode)]
private static extern uint ExtractIconEx(
string lpszFile,
int nIconIndex,
IntPtr[]? phiconLarge,
IntPtr[]? phiconSmall,
uint nIcons);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DestroyIcon(IntPtr hIcon);
[DllImport("gdi32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DeleteObject(IntPtr hObject);
[DllImport("ole32.dll")]
private static extern int CoInitializeEx(IntPtr pvReserved, uint dwCoInit);
[DllImport("ole32.dll")]
private static extern void CoUninitialize();
}

View File

@@ -0,0 +1,232 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using LanMontainDesktop.Models;
namespace LanMontainDesktop.Services;
public sealed class WindowsStartMenuService
{
private static readonly CompareInfo SortCompareInfo = CultureInfo.GetCultureInfo("zh-CN").CompareInfo;
private static readonly CompareOptions SortOptions =
CompareOptions.IgnoreCase |
CompareOptions.IgnoreKanaType |
CompareOptions.IgnoreWidth |
CompareOptions.StringSort;
private static readonly HashSet<string> SupportedEntryExtensions = new(StringComparer.OrdinalIgnoreCase)
{
".lnk",
".url",
".appref-ms"
};
public StartMenuFolderNode Load()
{
var root = new StartMenuFolderNode("All Apps", string.Empty);
if (!OperatingSystem.IsWindows())
{
return root;
}
foreach (var programsRoot in EnumerateProgramsRoots())
{
try
{
if (!Directory.Exists(programsRoot))
{
continue;
}
var scannedRoot = ScanFolder(programsRoot, programsRoot, "All Apps");
MergeFolder(root, scannedRoot);
}
catch
{
// Ignore unreadable start menu roots to keep launcher rendering resilient.
}
}
NormalizeFolderHierarchy(root);
SortFolder(root);
return root;
}
private static IEnumerable<string> EnumerateProgramsRoots()
{
var userStartMenu = Environment.GetFolderPath(Environment.SpecialFolder.StartMenu);
var commonStartMenu = Environment.GetFolderPath(Environment.SpecialFolder.CommonStartMenu);
var candidates = new[]
{
Path.Combine(userStartMenu, "Programs"),
Path.Combine(commonStartMenu, "Programs")
};
return candidates
.Where(path => !string.IsNullOrWhiteSpace(path))
.Distinct(StringComparer.OrdinalIgnoreCase);
}
private static StartMenuFolderNode ScanFolder(string folderPath, string rootPath, string? nameOverride = null)
{
var relativePath = Path.GetRelativePath(rootPath, folderPath);
if (string.Equals(relativePath, ".", StringComparison.Ordinal))
{
relativePath = string.Empty;
}
var folder = new StartMenuFolderNode(
nameOverride ?? Path.GetFileName(folderPath),
relativePath);
foreach (var subFolderPath in Directory.EnumerateDirectories(folderPath))
{
var folderName = Path.GetFileName(subFolderPath);
if (folderName.StartsWith(".", StringComparison.Ordinal))
{
continue;
}
folder.Folders.Add(ScanFolder(subFolderPath, rootPath));
}
foreach (var filePath in Directory.EnumerateFiles(folderPath))
{
var extension = Path.GetExtension(filePath);
if (!SupportedEntryExtensions.Contains(extension))
{
continue;
}
var fileName = Path.GetFileNameWithoutExtension(filePath);
if (string.IsNullOrWhiteSpace(fileName))
{
continue;
}
var normalizedName = fileName.Replace('_', ' ').Trim();
folder.Apps.Add(new StartMenuAppEntry
{
DisplayName = normalizedName,
FilePath = filePath,
RelativePath = Path.GetRelativePath(rootPath, filePath),
IconPngBytes = OperatingSystem.IsWindows()
? WindowsIconService.TryGetIconPngBytes(filePath)
: null
});
}
return folder;
}
private static void MergeFolder(StartMenuFolderNode target, StartMenuFolderNode source)
{
var appPathSet = new HashSet<string>(
target.Apps.Select(app => app.RelativePath),
StringComparer.OrdinalIgnoreCase);
foreach (var app in source.Apps)
{
if (appPathSet.Add(app.RelativePath))
{
target.Apps.Add(app);
}
}
foreach (var sourceFolder in source.Folders)
{
var existing = target.Folders.FirstOrDefault(folder =>
string.Equals(folder.Name, sourceFolder.Name, StringComparison.OrdinalIgnoreCase));
if (existing is null)
{
target.Folders.Add(sourceFolder);
continue;
}
MergeFolder(existing, sourceFolder);
}
}
private static void SortFolder(StartMenuFolderNode folder)
{
folder.Folders.Sort((left, right) => CompareDisplayName(left.Name, right.Name));
folder.Apps.Sort((left, right) => CompareDisplayName(left.DisplayName, right.DisplayName));
foreach (var child in folder.Folders)
{
SortFolder(child);
}
}
private static int CompareDisplayName(string? left, string? right)
{
var normalizedLeft = NormalizeForSort(left);
var normalizedRight = NormalizeForSort(right);
return SortCompareInfo.Compare(normalizedLeft, normalizedRight, SortOptions);
}
private static string NormalizeForSort(string? text)
{
if (string.IsNullOrWhiteSpace(text))
{
return "~";
}
return text.Trim();
}
private static void NormalizeFolderHierarchy(StartMenuFolderNode root)
{
if (root.Folders.Count == 0)
{
return;
}
var normalizedChildren = new List<StartMenuFolderNode>();
foreach (var child in root.Folders)
{
var normalizedChild = CollapseSingleChildFolders(child);
MergeIntoFolderList(normalizedChildren, normalizedChild);
}
root.Folders.Clear();
root.Folders.AddRange(normalizedChildren);
}
private static StartMenuFolderNode CollapseSingleChildFolders(StartMenuFolderNode folder)
{
if (folder.Folders.Count > 0)
{
var normalizedChildren = new List<StartMenuFolderNode>();
foreach (var child in folder.Folders)
{
var normalizedChild = CollapseSingleChildFolders(child);
MergeIntoFolderList(normalizedChildren, normalizedChild);
}
folder.Folders.Clear();
folder.Folders.AddRange(normalizedChildren);
}
while (folder.Apps.Count == 0 && folder.Folders.Count == 1)
{
folder = folder.Folders[0];
}
return folder;
}
private static void MergeIntoFolderList(List<StartMenuFolderNode> folders, StartMenuFolderNode source)
{
var existing = folders.FirstOrDefault(folder =>
string.Equals(folder.Name, source.Name, StringComparison.OrdinalIgnoreCase));
if (existing is null)
{
folders.Add(source);
return;
}
MergeFolder(existing, source);
}
}

View File

@@ -85,6 +85,14 @@
<Setter Property="BoxShadow" Value="0 2 4 #26000000" /> <Setter Property="BoxShadow" Value="0 2 4 #26000000" />
</Style> </Style>
<Style Selector="Border.mica-strong">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassStrongBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="12" />
<Setter Property="Opacity" Value="{DynamicResource AdaptiveGlassStrongOpacity}" />
<Setter Property="BoxShadow" Value="0 8 22 #2A000000" />
</Style>
<Style Selector="Border.glass-overlay"> <Style Selector="Border.glass-overlay">
<Setter Property="Background" Value="{DynamicResource AdaptiveGlassOverlayBackgroundBrush}" /> <Setter Property="Background" Value="{DynamicResource AdaptiveGlassOverlayBackgroundBrush}" />
<Setter Property="BorderThickness" Value="0" /> <Setter Property="BorderThickness" Value="0" />

View File

@@ -4,6 +4,7 @@ using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Threading; using Avalonia.Threading;
using LanMontainDesktop.ComponentSystem; using LanMontainDesktop.ComponentSystem;
@@ -298,14 +299,14 @@ public partial class MainWindow
TaskbarDynamicActionsPanel.Children.Clear(); TaskbarDynamicActionsPanel.Children.Clear();
} }
if (WallpaperPreviewTaskbarDynamicActionsPanel is not null) if (WallpaperPreviewTaskbarDynamicActionsHost is not null)
{ {
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Clear(); WallpaperPreviewTaskbarDynamicActionsHost.Children.Clear();
} }
if (actions.Count == 0 || if (actions.Count == 0 ||
TaskbarDynamicActionsPanel is null || TaskbarDynamicActionsPanel is null ||
WallpaperPreviewTaskbarDynamicActionsPanel is null) WallpaperPreviewTaskbarDynamicActionsHost is null)
{ {
return; return;
} }
@@ -341,7 +342,77 @@ public partial class MainWindow
BorderThickness = new Thickness(0), BorderThickness = new Thickness(0),
Child = previewText Child = previewText
}; };
WallpaperPreviewTaskbarDynamicActionsPanel.Children.Add(previewBorder); WallpaperPreviewTaskbarDynamicActionsHost.Children.Add(previewBorder);
}
}
private void PopulateComponentLibraryItems()
{
if (ComponentLibraryItemsPanel is null || ComponentLibraryEmptyTextBlock is null)
{
return;
}
ComponentLibraryItemsPanel.Children.Clear();
var definitions = _componentRegistry
.GetAll()
.Where(definition => definition.AllowDesktopPlacement)
.ToList();
foreach (var definition in definitions)
{
var title = new TextBlock
{
Text = definition.DisplayName,
FontWeight = FontWeight.SemiBold,
Foreground = Foreground
};
var details = new TextBlock
{
Text = $"Min {definition.MinWidthCells}x{definition.MinHeightCells}",
Foreground = Foreground,
Opacity = 0.8
};
var category = new TextBlock
{
Text = definition.Category,
Foreground = Foreground,
Opacity = 0.68
};
var content = new StackPanel
{
Spacing = 5,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
content.Children.Add(title);
content.Children.Add(details);
content.Children.Add(category);
var card = new Border
{
Classes = { "glass-panel" },
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(12, 10),
Margin = new Thickness(0, 0, 10, 10),
Width = 180,
MinHeight = 92,
Child = content
};
ComponentLibraryItemsPanel.Children.Add(card);
}
var hasAny = definitions.Count > 0;
ComponentLibraryEmptyTextBlock.IsVisible = !hasAny;
if (ComponentLibraryItemsScrollViewer is not null)
{
ComponentLibraryItemsScrollViewer.IsVisible = hasAny;
} }
} }
} }

View File

@@ -0,0 +1,785 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using Avalonia.VisualTree;
using LanMontainDesktop.Models;
using LanMontainDesktop.Services;
namespace LanMontainDesktop.Views;
public partial class MainWindow
{
private const int MinDesktopPageCount = 1;
private const int MaxDesktopPageCount = 12;
private readonly WindowsStartMenuService _windowsStartMenuService = new();
private readonly Dictionary<string, Bitmap> _launcherIconCache = new(StringComparer.OrdinalIgnoreCase);
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
private Bitmap? _launcherFolderIconBitmap;
private int _desktopPageCount = MinDesktopPageCount;
private int _currentDesktopSurfaceIndex;
private double _desktopSurfacePageWidth;
private TranslateTransform? _desktopPagesHostTransform;
private bool _isDesktopSwipeActive;
private Point _desktopSwipeStartPoint;
private Point _desktopSwipeCurrentPoint;
private double _desktopSwipeBaseOffset;
private int LauncherSurfaceIndex => Math.Max(MinDesktopPageCount, _desktopPageCount);
private int TotalSurfaceCount => LauncherSurfaceIndex + 1;
private void InitializeDesktopSurfaceState(AppSettingsSnapshot snapshot)
{
var loadedPageCount = snapshot.DesktopPageCount <= 0 ? MinDesktopPageCount : snapshot.DesktopPageCount;
_desktopPageCount = Math.Clamp(loadedPageCount, MinDesktopPageCount, MaxDesktopPageCount);
_currentDesktopSurfaceIndex = Math.Clamp(snapshot.CurrentDesktopSurfaceIndex, 0, LauncherSurfaceIndex);
}
private async void LoadLauncherEntriesAsync()
{
try
{
var loadResult = await Task.Run(() =>
{
var loadedRoot = _windowsStartMenuService.Load();
var folderIconBytes = OperatingSystem.IsWindows()
? WindowsIconService.TryGetSystemFolderIconPngBytes()
: null;
return (Root: loadedRoot, FolderIcon: folderIconBytes);
});
await Dispatcher.UIThread.InvokeAsync(() =>
{
_startMenuRoot = loadResult.Root;
_launcherFolderIconPngBytes = loadResult.FolderIcon;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
RenderLauncherRootTiles();
}, DispatcherPriority.Background);
}
catch
{
_startMenuRoot = new StartMenuFolderNode("All Apps", string.Empty);
_launcherFolderIconPngBytes = null;
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
RenderLauncherRootTiles();
}
}
private void UpdateDesktopSurfaceLayout(GridMetrics gridMetrics)
{
if (DesktopPagesViewport is null ||
DesktopPagesHost is null ||
DesktopPagesContainer is null ||
LauncherPagePanel is null)
{
return;
}
_desktopPagesHostTransform = DesktopPagesHost.RenderTransform as TranslateTransform;
if (_desktopPagesHostTransform is null)
{
_desktopPagesHostTransform = new TranslateTransform();
DesktopPagesHost.RenderTransform = _desktopPagesHostTransform;
}
var viewportRow = gridMetrics.RowCount > 2 ? 1 : 0;
var viewportRowSpan = gridMetrics.RowCount > 2 ? gridMetrics.RowCount - 2 : 1;
var pageWidth = Math.Max(1, gridMetrics.ColumnCount * gridMetrics.CellSize);
var pageHeight = Math.Max(1, viewportRowSpan * gridMetrics.CellSize);
Grid.SetRow(DesktopPagesViewport, viewportRow);
Grid.SetColumn(DesktopPagesViewport, 0);
Grid.SetRowSpan(DesktopPagesViewport, viewportRowSpan);
Grid.SetColumnSpan(DesktopPagesViewport, gridMetrics.ColumnCount);
DesktopPagesViewport.Width = pageWidth;
DesktopPagesViewport.Height = pageHeight;
DesktopPagesHost.RowDefinitions.Clear();
DesktopPagesHost.RowDefinitions.Add(new RowDefinition(new GridLength(pageHeight, GridUnitType.Pixel)));
DesktopPagesHost.ColumnDefinitions.Clear();
DesktopPagesHost.ColumnDefinitions.Add(
new ColumnDefinition(new GridLength(pageWidth * _desktopPageCount, GridUnitType.Pixel)));
DesktopPagesHost.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel)));
DesktopPagesHost.Width = pageWidth * TotalSurfaceCount;
DesktopPagesHost.Height = pageHeight;
DesktopPagesContainer.RowDefinitions.Clear();
DesktopPagesContainer.RowDefinitions.Add(new RowDefinition(new GridLength(pageHeight, GridUnitType.Pixel)));
DesktopPagesContainer.ColumnDefinitions.Clear();
DesktopPagesContainer.Children.Clear();
DesktopPagesContainer.Width = pageWidth * _desktopPageCount;
DesktopPagesContainer.Height = pageHeight;
for (var index = 0; index < _desktopPageCount; index++)
{
DesktopPagesContainer.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(pageWidth, GridUnitType.Pixel)));
var pageSurface = new Border
{
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(10)
};
if (_desktopPageCount > 1)
{
pageSurface.Child = new TextBlock
{
HorizontalAlignment = HorizontalAlignment.Right,
VerticalAlignment = VerticalAlignment.Top,
Foreground = Foreground,
Opacity = 0.72,
Text = Lf("desktop.page_index_format", "Desktop {0}", index + 1)
};
}
Grid.SetColumn(pageSurface, index);
Grid.SetRow(pageSurface, 0);
DesktopPagesContainer.Children.Add(pageSurface);
}
Grid.SetColumn(LauncherPagePanel, 1);
Grid.SetRow(LauncherPagePanel, 0);
// 为启动台添加安全边距以确保圆角不被裁剪
var launcherMargin = Math.Clamp(gridMetrics.CellSize * 0.15, 6, 16);
LauncherPagePanel.Margin = new Thickness(launcherMargin);
LauncherPagePanel.Width = Math.Max(1, pageWidth - launcherMargin * 2);
LauncherPagePanel.Height = Math.Max(1, pageHeight - launcherMargin * 2);
LauncherPagePanel.MaxWidth = pageWidth - launcherMargin * 2;
LauncherPagePanel.MaxHeight = pageHeight - launcherMargin * 2;
if (LauncherFolderPanel is not null)
{
LauncherFolderPanel.MaxWidth = Math.Max(320, pageWidth - 96);
LauncherFolderPanel.MaxHeight = Math.Max(220, pageHeight - 96);
}
// 更新启动台图标布局
UpdateLauncherTileLayout();
_desktopSurfacePageWidth = pageWidth;
ClampSurfaceIndex();
ApplyDesktopSurfaceOffset();
}
private void UpdateLauncherTileLayout()
{
if (LauncherRootTilePanel is null || LauncherPagePanel is null)
{
return;
}
// 获取启动台面板的实际可用宽度减去Padding
var availableWidth = Math.Max(1, LauncherPagePanel.Bounds.Width - 36); // 18px padding on each side
var availableHeight = Math.Max(1, LauncherPagePanel.Bounds.Height - 100); // 预留标题空间
if (availableWidth <= 1 || availableHeight <= 1)
{
// 如果尺寸还未计算,使用默认值
availableWidth = 600;
availableHeight = 400;
}
// 计算最佳图标尺寸
// 目标每行显示4-8个图标根据屏幕宽度调整
const int minColumns = 4;
const int maxColumns = 8;
const double targetAspectRatio = 1.2; // 图标宽高比
// 计算每列可以显示的图标数量
var optimalColumnCount = Math.Clamp((int)Math.Floor(availableWidth / 120), minColumns, maxColumns);
// 根据列数计算图标尺寸
var tileWidth = Math.Floor(availableWidth / optimalColumnCount) - 12; // 12px spacing
var tileHeight = Math.Min(tileWidth / targetAspectRatio, availableHeight / 4); // 至少显示4行
// 确保最小尺寸
tileWidth = Math.Max(tileWidth, 100);
tileHeight = Math.Max(tileHeight, 80);
// 更新WrapPanel的Item尺寸
LauncherRootTilePanel.Width = availableWidth;
// 更新所有子元素的尺寸
foreach (var child in LauncherRootTilePanel.Children)
{
if (child is Button button)
{
button.Width = tileWidth;
button.Height = tileHeight;
}
}
// 同样更新文件夹视图的图标尺寸
if (LauncherFolderTilePanel is not null)
{
LauncherFolderTilePanel.Width = availableWidth;
foreach (var child in LauncherFolderTilePanel.Children)
{
if (child is Button button)
{
button.Width = tileWidth;
button.Height = tileHeight;
}
}
}
}
private void ClampSurfaceIndex()
{
_currentDesktopSurfaceIndex = Math.Clamp(_currentDesktopSurfaceIndex, 0, LauncherSurfaceIndex);
}
private IBrush GetThemeBrush(string key)
{
if (Resources.TryGetResource(key, ActualThemeVariant, out var resource) && resource is IBrush brush)
{
return brush;
}
return Brushes.Transparent;
}
private void ApplyDesktopSurfaceOffset()
{
if (_desktopPagesHostTransform is null || _desktopSurfacePageWidth <= 0)
{
return;
}
var targetOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
_desktopPagesHostTransform.X = targetOffset;
if (_currentDesktopSurfaceIndex != LauncherSurfaceIndex)
{
CloseLauncherFolderOverlay();
}
}
private void MoveSurfaceBy(int delta)
{
if (delta == 0)
{
return;
}
var target = Math.Clamp(_currentDesktopSurfaceIndex + delta, 0, LauncherSurfaceIndex);
if (target == _currentDesktopSurfaceIndex)
{
ApplyDesktopSurfaceOffset();
return;
}
_currentDesktopSurfaceIndex = target;
ApplyDesktopSurfaceOffset();
PersistSettings();
}
private bool CanSwipeDesktopSurface()
{
return !_isSettingsOpen && !_isComponentLibraryOpen && _desktopSurfacePageWidth > 1;
}
private void OnDesktopPagesPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!CanSwipeDesktopSurface() || DesktopPagesViewport is null)
{
return;
}
if (IsInteractivePointerSource(e.Source))
{
return;
}
if (!e.GetCurrentPoint(DesktopPagesViewport).Properties.IsLeftButtonPressed)
{
return;
}
_isDesktopSwipeActive = true;
_desktopSwipeStartPoint = e.GetPosition(DesktopPagesViewport);
_desktopSwipeCurrentPoint = _desktopSwipeStartPoint;
_desktopSwipeBaseOffset = -_currentDesktopSurfaceIndex * _desktopSurfacePageWidth;
e.Pointer.Capture(DesktopPagesViewport);
}
private static bool IsInteractivePointerSource(object? source)
{
if (source is not Visual visual)
{
return false;
}
foreach (var node in visual.GetSelfAndVisualAncestors())
{
if (node is Button or TextBox or ComboBox or ListBoxItem or Slider or ToggleSwitch)
{
return true;
}
}
return false;
}
private void OnDesktopPagesPointerMoved(object? sender, PointerEventArgs e)
{
if (!_isDesktopSwipeActive || DesktopPagesViewport is null || _desktopPagesHostTransform is null)
{
return;
}
_desktopSwipeCurrentPoint = e.GetPosition(DesktopPagesViewport);
var deltaX = _desktopSwipeCurrentPoint.X - _desktopSwipeStartPoint.X;
var minOffset = -LauncherSurfaceIndex * _desktopSurfacePageWidth;
var tentative = _desktopSwipeBaseOffset + deltaX;
_desktopPagesHostTransform.X = Math.Clamp(tentative, minOffset, 0);
e.Handled = true;
}
private void OnDesktopPagesPointerReleased(object? sender, PointerReleasedEventArgs e)
{
EndDesktopSwipeInteraction(e.Pointer);
}
private void OnDesktopPagesPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
EndDesktopSwipeInteraction(e.Pointer);
}
private void EndDesktopSwipeInteraction(IPointer? pointer)
{
if (!_isDesktopSwipeActive)
{
return;
}
_isDesktopSwipeActive = false;
if (pointer?.Captured == DesktopPagesViewport)
{
pointer.Capture(null);
}
var deltaX = _desktopSwipeCurrentPoint.X - _desktopSwipeStartPoint.X;
var deltaY = _desktopSwipeCurrentPoint.Y - _desktopSwipeStartPoint.Y;
var threshold = Math.Max(56, _desktopSurfacePageWidth * 0.16);
if (Math.Abs(deltaX) >= threshold && Math.Abs(deltaX) > Math.Abs(deltaY))
{
MoveSurfaceBy(deltaX < 0 ? 1 : -1);
return;
}
ApplyDesktopSurfaceOffset();
}
private void OnDesktopPagesPointerWheelChanged(object? sender, PointerWheelEventArgs e)
{
if (!CanSwipeDesktopSurface())
{
return;
}
var prefersHorizontal = Math.Abs(e.Delta.X) > Math.Abs(e.Delta.Y) ||
e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (!prefersHorizontal)
{
return;
}
var delta = e.Delta.X != 0 ? e.Delta.X : e.Delta.Y;
if (Math.Abs(delta) < double.Epsilon)
{
return;
}
MoveSurfaceBy(delta < 0 ? 1 : -1);
e.Handled = true;
}
private void RenderLauncherRootTiles()
{
if (LauncherRootTilePanel is null)
{
return;
}
LauncherRootTilePanel.Children.Clear();
var folders = _startMenuRoot.Folders;
var apps = _startMenuRoot.Apps;
foreach (var folder in folders)
{
LauncherRootTilePanel.Children.Add(CreateLauncherFolderTile(folder));
}
foreach (var app in apps)
{
LauncherRootTilePanel.Children.Add(CreateLauncherAppTile(app));
}
if (LauncherRootTilePanel.Children.Count == 0)
{
LauncherRootTilePanel.Children.Add(CreateLauncherHintTile(
L("launcher.empty", "No Start Menu entries found."),
string.Empty));
}
// 在图标渲染完成后,应用布局计算
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
}
private Button CreateLauncherFolderTile(StartMenuFolderNode folder)
{
var title = folder.Name;
var subtitle = Lf("launcher.folder_items_format", "{0} apps", folder.TotalAppCount);
var folderIconBitmap = GetLauncherFolderIconBitmap();
return CreateLauncherTileButton(
title,
subtitle,
monogram: "DIR",
iconBitmap: folderIconBitmap,
() => OpenLauncherFolder(folder));
}
private Button CreateLauncherAppTile(StartMenuAppEntry app)
{
var iconBitmap = GetLauncherIconBitmap(app);
var monogram = BuildMonogram(app.DisplayName);
return CreateLauncherTileButton(
app.DisplayName,
subtitle: string.Empty,
monogram,
iconBitmap,
() => LaunchStartMenuEntry(app));
}
private Control CreateLauncherHintTile(string title, string subtitle)
{
var panel = new StackPanel
{
Spacing = 6,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
panel.Children.Add(new TextBlock
{
Text = title,
FontWeight = FontWeight.SemiBold,
HorizontalAlignment = HorizontalAlignment.Center
});
if (!string.IsNullOrWhiteSpace(subtitle))
{
panel.Children.Add(new TextBlock
{
Text = subtitle,
Opacity = 0.75,
HorizontalAlignment = HorizontalAlignment.Center
});
}
return new Border
{
Classes = { "glass-panel" },
BorderThickness = new Thickness(0),
Margin = new Thickness(0, 0, 12, 12),
CornerRadius = new CornerRadius(12),
Child = panel
// 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置
};
}
private Button CreateLauncherTileButton(
string title,
string subtitle,
string monogram,
Bitmap? iconBitmap,
Action clickAction)
{
Control iconControl = iconBitmap is not null
? new Image
{
Source = iconBitmap,
Width = 40,
Height = 40,
Stretch = Stretch.Uniform
}
: new Border
{
Width = 40,
Height = 40,
CornerRadius = new CornerRadius(999),
Background = GetThemeBrush("AdaptiveButtonBackgroundBrush"),
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center,
BorderThickness = new Thickness(0),
Child = new TextBlock
{
Text = monogram,
FontWeight = FontWeight.Bold,
HorizontalAlignment = HorizontalAlignment.Center,
VerticalAlignment = VerticalAlignment.Center
}
};
var textPanel = new StackPanel
{
Spacing = 3,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
};
textPanel.Children.Add(new TextBlock
{
Text = title,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
TextAlignment = TextAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
});
if (!string.IsNullOrWhiteSpace(subtitle))
{
textPanel.Children.Add(new TextBlock
{
Text = subtitle,
Opacity = 0.72,
TextTrimming = TextTrimming.CharacterEllipsis,
TextAlignment = TextAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Stretch
});
}
var content = new StackPanel
{
Spacing = 8,
VerticalAlignment = VerticalAlignment.Center
};
content.Children.Add(iconControl);
content.Children.Add(textPanel);
var button = new Button
{
Classes = { "glass-panel" },
Margin = new Thickness(0, 0, 12, 12),
BorderThickness = new Thickness(0),
CornerRadius = new CornerRadius(12),
Padding = new Thickness(10),
Content = content
// 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置
};
button.Click += (_, _) => clickAction();
return button;
}
private Bitmap? GetLauncherIconBitmap(StartMenuAppEntry app)
{
if (app.IconPngBytes is null || app.IconPngBytes.Length == 0)
{
return null;
}
if (_launcherIconCache.TryGetValue(app.RelativePath, out var cached))
{
return cached;
}
try
{
using var stream = new MemoryStream(app.IconPngBytes, writable: false);
var bitmap = new Bitmap(stream);
_launcherIconCache[app.RelativePath] = bitmap;
return bitmap;
}
catch
{
return null;
}
}
private Bitmap? GetLauncherFolderIconBitmap()
{
if (_launcherFolderIconBitmap is not null)
{
return _launcherFolderIconBitmap;
}
if (_launcherFolderIconPngBytes is null || _launcherFolderIconPngBytes.Length == 0)
{
return null;
}
try
{
using var stream = new MemoryStream(_launcherFolderIconPngBytes, writable: false);
_launcherFolderIconBitmap = new Bitmap(stream);
return _launcherFolderIconBitmap;
}
catch
{
_launcherFolderIconBitmap = null;
return null;
}
}
private void OpenLauncherFolder(StartMenuFolderNode folder)
{
_launcherFolderStack.Push(folder);
RenderLauncherFolderFromStack();
}
private void CloseLauncherFolderOverlay()
{
_launcherFolderStack.Clear();
if (LauncherFolderOverlay is not null)
{
LauncherFolderOverlay.IsVisible = false;
}
if (LauncherFolderTilePanel is not null)
{
LauncherFolderTilePanel.Children.Clear();
}
}
private void RenderLauncherFolderFromStack()
{
if (LauncherFolderOverlay is null ||
LauncherFolderTilePanel is null ||
LauncherFolderTitleTextBlock is null ||
LauncherFolderBackButton is null)
{
return;
}
if (_launcherFolderStack.Count == 0)
{
CloseLauncherFolderOverlay();
return;
}
var folder = _launcherFolderStack.Peek();
LauncherFolderOverlay.IsVisible = true;
LauncherFolderTitleTextBlock.Text = folder.Name;
LauncherFolderBackButton.IsVisible = _launcherFolderStack.Count > 1;
LauncherFolderTilePanel.Children.Clear();
foreach (var subFolder in folder.Folders)
{
LauncherFolderTilePanel.Children.Add(CreateLauncherFolderTile(subFolder));
}
foreach (var app in folder.Apps)
{
LauncherFolderTilePanel.Children.Add(CreateLauncherAppTile(app));
}
if (LauncherFolderTilePanel.Children.Count == 0)
{
LauncherFolderTilePanel.Children.Add(CreateLauncherHintTile(
L("launcher.empty_folder", "This folder is empty."),
string.Empty));
}
// 在图标渲染完成后,应用布局计算
Dispatcher.UIThread.Post(() => UpdateLauncherTileLayout(), DispatcherPriority.Background);
}
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();
}
private static void LaunchStartMenuEntry(StartMenuAppEntry app)
{
try
{
var startInfo = new ProcessStartInfo
{
FileName = app.FilePath,
UseShellExecute = true
};
Process.Start(startInfo);
}
catch
{
// Ignore failures to launch malformed shortcuts.
}
}
private void OnLauncherFolderBackClick(object? sender, RoutedEventArgs e)
{
if (_launcherFolderStack.Count <= 1)
{
CloseLauncherFolderOverlay();
return;
}
_launcherFolderStack.Pop();
RenderLauncherFolderFromStack();
}
private void OnLauncherFolderOverlayPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (LauncherFolderPanel is null)
{
return;
}
var point = e.GetCurrentPoint(LauncherFolderPanel).Position;
if (point.X >= 0 &&
point.Y >= 0 &&
point.X <= LauncherFolderPanel.Bounds.Width &&
point.Y <= LauncherFolderPanel.Bounds.Height)
{
return;
}
CloseLauncherFolderOverlay();
e.Handled = true;
}
private void OnLauncherFolderCloseClick(object? sender, RoutedEventArgs e)
{
CloseLauncherFolderOverlay();
}
private void DisposeLauncherResources()
{
foreach (var bitmap in _launcherIconCache.Values)
{
bitmap.Dispose();
}
_launcherIconCache.Clear();
_launcherFolderIconBitmap?.Dispose();
_launcherFolderIconBitmap = null;
}
}

View File

@@ -35,7 +35,7 @@ public partial class MainWindow
{ {
return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase) return string.Equals(languageCode, "en-US", StringComparison.OrdinalIgnoreCase)
? L("settings.region.language_en", "English") ? L("settings.region.language_en", "English")
: L("settings.region.language_zh", "中文"); : L("settings.region.language_zh", "Chinese");
} }
private string GetLocalizedPlacementDisplayName(WallpaperPlacement placement) private string GetLocalizedPlacementDisplayName(WallpaperPlacement placement)
@@ -58,16 +58,21 @@ public partial class MainWindow
BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows"); BackToWindowsTextBlock.Text = L("button.back_to_windows", "Back to Windows");
WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows"); WallpaperPreviewBackButtonTextBlock.Text = L("button.back_to_windows", "Back to Windows");
ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows")); ToolTip.SetTip(BackToWindowsButton, L("tooltip.back_to_windows", "Back to Windows"));
OpenComponentLibraryTextBlock.Text = L("button.component_library", "组件库");
WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "组件库"); OpenComponentLibraryTextBlock.Text = L("button.component_library", "Component Library");
ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "组件库")); WallpaperPreviewComponentLibraryTextBlock.Text = L("button.component_library", "Component Library");
ComponentLibraryTitleTextBlock.Text = L("component_library.title", "组件库"); ToolTip.SetTip(OpenComponentLibraryButton, L("tooltip.component_library", "Component Library"));
ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "关闭")); ComponentLibraryTitleTextBlock.Text = L("component_library.title", "Component Library");
ToolTip.SetTip(CloseComponentLibraryButton, L("common.close", "Close"));
ComponentLibraryEmptyTextBlock.Text = L( ComponentLibraryEmptyTextBlock.Text = L(
"component_library.empty", "component_library.empty",
"暂无组件,后续将在这里显示。"); "No components yet. Components will appear here later.");
LauncherTitleTextBlock.Text = L("launcher.title", "应用启动台");
LauncherSubtitleTextBlock.Text = L("launcher.subtitle", "按 Windows 开始菜单结构显示所有应用与文件夹");
ToolTip.SetTip(LauncherFolderBackButton, L("common.back", "返回"));
ToolTip.SetTip(LauncherFolderCloseButton, L("common.close", "关闭"));
SettingsTitleTextBlock.Text = L("settings.title", "Settings");
SettingsNavHeaderTextBlock.Text = L("settings.nav_header", "Settings"); SettingsNavHeaderTextBlock.Text = L("settings.nav_header", "Settings");
SettingsNavWallpaperItem.Content = L("settings.nav.wallpaper", "Wallpaper"); SettingsNavWallpaperItem.Content = L("settings.nav.wallpaper", "Wallpaper");
SettingsNavGridItem.Content = L("settings.nav.grid", "Grid"); SettingsNavGridItem.Content = L("settings.nav.grid", "Grid");
@@ -75,21 +80,18 @@ public partial class MainWindow
SettingsNavStatusBarItem.Content = L("settings.nav.status_bar", "Status Bar"); SettingsNavStatusBarItem.Content = L("settings.nav.status_bar", "Status Bar");
SettingsNavRegionItem.Content = L("settings.nav.region", "Region"); SettingsNavRegionItem.Content = L("settings.nav.region", "Region");
WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "Wallpaper"); WallpaperPanelTitleTextBlock.Text = L("settings.wallpaper.title", "个性化您的背景");
WallpaperPanelDescriptionTextBlock.Text = L("settings.wallpaper.description", "Pick wallpaper."); WallpaperPanelDescriptionTextBlock.Text = L("settings.wallpaper.description", "选择图片或视频");
WallpaperCurrentLabelTextBlock.Text = L("settings.wallpaper.current_label", "Current Wallpaper"); WallpaperPlacementSettingsExpander.Header = L("settings.wallpaper.placement_label", "选择契合度");
WallpaperPlacementLabelTextBlock.Text = L("settings.wallpaper.placement_label", "Placement"); WallpaperPlacementSettingsExpander.Description = L("settings.wallpaper.placement_desc", "调整图像在桌面上的填充方式。");
PickWallpaperButton.Content = L("settings.wallpaper.pick_button", "Browse Files"); PickWallpaperButton.Content = L("settings.wallpaper.pick_button", "浏览照片");
ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "Reset"); ClearWallpaperButton.Content = L("settings.wallpaper.clear_button", "重置");
GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout"); GridPanelTitleTextBlock.Text = L("settings.grid.title", "Grid Layout");
GridPanelDescriptionTextBlock.Text = L("settings.grid.description", "Each component should occupy at least 1x1.");
GridShortSideLabelTextBlock.Text = L("settings.grid.short_side_label", "Short Side Cells");
ApplyGridButton.Content = L("settings.grid.apply_button", "Apply"); ApplyGridButton.Content = L("settings.grid.apply_button", "Apply");
ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color"); ColorPanelTitleTextBlock.Text = L("settings.color.title", "Color");
ColorPanelDescriptionTextBlock.Text = L("settings.color.description", "Theme and accent settings."); ThemeModeSettingsExpander.Header = L("settings.color.day_night_label", "Day/Night");
DayNightModeLabelTextBlock.Text = L("settings.color.day_night_label", "Day/Night");
NightModeToggleSwitch.OnContent = L("settings.color.day_night_on", "Night"); NightModeToggleSwitch.OnContent = L("settings.color.day_night_on", "Night");
NightModeToggleSwitch.OffContent = L("settings.color.day_night_off", "Day"); NightModeToggleSwitch.OffContent = L("settings.color.day_night_off", "Day");
RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors"); RecommendedColorsLabelTextBlock.Text = L("settings.color.recommended_label", "Recommended Colors");
@@ -97,15 +99,11 @@ public partial class MainWindow
RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh"); RefreshMonetColorsButton.Content = L("settings.color.refresh_button", "Refresh");
StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar"); StatusBarPanelTitleTextBlock.Text = L("settings.status_bar.title", "Status Bar");
StatusBarPanelDescriptionTextBlock.Text = L("settings.status_bar.description", "Status bar components.");
StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock"); StatusBarClockSettingsExpander.Header = L("settings.status_bar.clock_header", "Clock");
StatusBarClockDescriptionTextBlock.Text = L("settings.status_bar.clock_description", "Display clock in top status bar.");
RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region"); RegionPanelTitleTextBlock.Text = L("settings.region.title", "Region");
RegionPanelDescriptionTextBlock.Text = L("settings.region.description", "Select language.");
LanguageSettingsExpander.Header = L("settings.region.language_header", "Language"); LanguageSettingsExpander.Header = L("settings.region.language_header", "Language");
LanguageLabelTextBlock.Text = L("settings.region.language_label", "Language"); LanguageChineseItem.Content = L("settings.region.language_zh", "Chinese");
LanguageChineseItem.Content = L("settings.region.language_zh", "中文");
LanguageEnglishItem.Content = L("settings.region.language_en", "English"); LanguageEnglishItem.Content = L("settings.region.language_en", "English");
if (WallpaperPlacementComboBox?.ItemCount >= 5) if (WallpaperPlacementComboBox?.ItemCount >= 5)
@@ -117,9 +115,6 @@ public partial class MainWindow
if (WallpaperPlacementComboBox.Items[4] is ComboBoxItem tileItem) tileItem.Content = L("placement.tile", "Tile"); if (WallpaperPlacementComboBox.Items[4] is ComboBoxItem tileItem) tileItem.Content = L("placement.tile", "Tile");
} }
ThemeModeStatusTextBlock.Text = _isNightMode
? L("settings.color.mode_night", "Night mode enabled")
: L("settings.color.mode_day", "Day mode enabled");
GridInfoTextBlock.Text = Lf( GridInfoTextBlock.Text = Lf(
"settings.grid.info_format", "settings.grid.info_format",
@@ -128,6 +123,8 @@ public partial class MainWindow
DesktopGrid.RowDefinitions.Count, DesktopGrid.RowDefinitions.Count,
DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d); DesktopGrid.RowDefinitions.Count > 0 ? DesktopGrid.RowDefinitions[0].Height.Value : 0d);
PopulateComponentLibraryItems();
RenderLauncherRootTiles();
UpdateOpenSettingsActionVisualState(); UpdateOpenSettingsActionVisualState();
UpdateWallpaperDisplay(); UpdateWallpaperDisplay();
} }

View File

@@ -1,4 +1,7 @@
using System; using System;
using FluentIcons.Avalonia;
using FluentIcons.Common;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@@ -302,6 +305,21 @@ public partial class MainWindow
TileMode = TileMode.None TileMode = TileMode.None
}; };
if (forPreview)
{
// For preview, we want to simulate how the image looks on a real screen.
// Assuming a nominal screen width of 1920 for calculation.
// The preview width is 480, so the scale is 480/1920 = 0.25.
const double nominalScreenWidth = 1920.0;
const double previewWidth = 480.0;
double scale = previewWidth / nominalScreenWidth;
if (placement == WallpaperPlacement.Center)
{
brush.Transform = new ScaleTransform(scale, scale);
}
}
switch (placement) switch (placement)
{ {
case WallpaperPlacement.Fill: case WallpaperPlacement.Fill:
@@ -618,7 +636,9 @@ public partial class MainWindow
TopStatusComponentIds = _topStatusComponentIds.ToList(), TopStatusComponentIds = _topStatusComponentIds.ToList(),
PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(), PinnedTaskbarActions = _pinnedTaskbarActions.Select(action => action.ToString()).ToList(),
EnableDynamicTaskbarActions = _enableDynamicTaskbarActions, EnableDynamicTaskbarActions = _enableDynamicTaskbarActions,
TaskbarLayoutMode = _taskbarLayoutMode TaskbarLayoutMode = _taskbarLayoutMode,
DesktopPageCount = _desktopPageCount,
CurrentDesktopSurfaceIndex = _currentDesktopSurfaceIndex
}; };
_appSettingsService.Save(snapshot); _appSettingsService.Save(snapshot);
@@ -670,13 +690,11 @@ public partial class MainWindow
{ {
_isNightMode = enabled; _isNightMode = enabled;
RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light; RequestedThemeVariant = enabled ? ThemeVariant.Dark : ThemeVariant.Light;
UpdateThemeModeIcon();
_suppressThemeToggleEvents = true; _suppressThemeToggleEvents = true;
NightModeToggleSwitch.IsChecked = enabled; NightModeToggleSwitch.IsChecked = enabled;
_suppressThemeToggleEvents = false; _suppressThemeToggleEvents = false;
ThemeModeStatusTextBlock.Text = enabled
? L("settings.color.mode_night", "Night mode enabled")
: L("settings.color.mode_day", "Day mode enabled");
if (refreshPalettes) if (refreshPalettes)
{ {
@@ -1001,4 +1019,70 @@ public partial class MainWindow
SettingsPage.IsVisible = false; SettingsPage.IsVisible = false;
}, TimeSpan.FromMilliseconds(200)); }, TimeSpan.FromMilliseconds(200));
} }
private void InitializeSettingsIcons()
{
const IconVariant variant = IconVariant.Regular;
if (WallpaperPlacementSettingsExpander is not null)
{
WallpaperPlacementSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Image,
IconVariant = variant
};
}
if (GridSizeSettingsExpander is not null)
{
GridSizeSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Grid,
IconVariant = variant
};
}
if (ThemeColorSettingsExpander is not null)
{
ThemeColorSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Color,
IconVariant = variant
};
}
if (StatusBarClockSettingsExpander is not null)
{
StatusBarClockSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Clock,
IconVariant = variant
};
}
if (LanguageSettingsExpander is not null)
{
LanguageSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = Symbol.Earth,
IconVariant = variant
};
}
UpdateThemeModeIcon();
}
private void UpdateThemeModeIcon()
{
if (ThemeModeSettingsExpander is null)
{
return;
}
ThemeModeSettingsExpander.IconSource = new FluentIcons.Avalonia.Fluent.SymbolIconSource
{
Symbol = _isNightMode ? Symbol.WeatherMoon : Symbol.WeatherSunny,
IconVariant = IconVariant.Regular
};
}
} }

File diff suppressed because it is too large Load Diff

View File

@@ -7,6 +7,7 @@ using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform; using Avalonia.Platform;
@@ -130,6 +131,8 @@ public partial class MainWindow : Window
_defaultDesktopBackground = DesktopWallpaperLayer.Background; _defaultDesktopBackground = DesktopWallpaperLayer.Background;
ApplyTaskbarSettings(snapshot); ApplyTaskbarSettings(snapshot);
InitializeLocalization(snapshot.LanguageCode); InitializeLocalization(snapshot.LanguageCode);
InitializeDesktopSurfaceState(snapshot);
InitializeSettingsIcons();
TryRestoreWallpaper(snapshot.WallpaperPath); TryRestoreWallpaper(snapshot.WallpaperPath);
ApplyWallpaperBrush(); ApplyWallpaperBrush();
@@ -151,6 +154,8 @@ public partial class MainWindow : Window
DesktopHost.SizeChanged += OnDesktopHostSizeChanged; DesktopHost.SizeChanged += OnDesktopHostSizeChanged;
WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged; WallpaperPreviewHost.SizeChanged += OnWallpaperPreviewHostSizeChanged;
RebuildDesktopGrid(); RebuildDesktopGrid();
PopulateComponentLibraryItems();
LoadLauncherEntriesAsync();
_suppressSettingsPersistence = false; _suppressSettingsPersistence = false;
PersistSettings(); PersistSettings();
@@ -164,6 +169,7 @@ public partial class MainWindow : Window
_previewVideoWallpaperMedia = null; _previewVideoWallpaperMedia = null;
_previewVideoWallpaperPlayer?.Dispose(); _previewVideoWallpaperPlayer?.Dispose();
_previewVideoWallpaperPlayer = null; _previewVideoWallpaperPlayer = null;
DisposeLauncherResources();
_videoWallpaperMedia?.Dispose(); _videoWallpaperMedia?.Dispose();
_videoWallpaperMedia = null; _videoWallpaperMedia = null;
_videoWallpaperPlayer?.Dispose(); _videoWallpaperPlayer?.Dispose();
@@ -257,6 +263,7 @@ public partial class MainWindow : Window
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
ApplyWidgetSizing(gridMetrics.CellSize); ApplyWidgetSizing(gridMetrics.CellSize);
UpdateDesktopSurfaceLayout(gridMetrics);
UpdateSettingsViewportInsets(gridMetrics.CellSize); UpdateSettingsViewportInsets(gridMetrics.CellSize);
GridInfoTextBlock.Text = Lf( GridInfoTextBlock.Text = Lf(
@@ -378,7 +385,7 @@ public partial class MainWindow : Window
private void UpdateSettingsViewportInsets(double cellSize) private void UpdateSettingsViewportInsets(double cellSize)
{ {
if (SettingsBackdropOverlay is null || SettingsContentPanel is null) if (SettingsContentPanel is null)
{ {
return; return;
} }
@@ -388,16 +395,29 @@ public partial class MainWindow : Window
var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18); var verticalGap = Math.Clamp(clampedCell * 0.16, 6, 18);
var topInset = clampedCell + verticalGap; var topInset = clampedCell + verticalGap;
var bottomInset = clampedCell + verticalGap; var bottomInset = clampedCell + verticalGap;
var inset = new Thickness(horizontalInset, topInset, horizontalInset, bottomInset);
SettingsBackdropOverlay.Margin = inset; // 添加额外的安全边距以确保圆角不被裁剪
var cornerSafetyMargin = Math.Clamp(clampedCell * 0.12, 4, 12);
var inset = new Thickness(
horizontalInset + cornerSafetyMargin,
topInset + cornerSafetyMargin,
horizontalInset + cornerSafetyMargin,
bottomInset + cornerSafetyMargin);
// 使用 Margin 来定位,而不是直接设置 Width/Height
// 这样可以让面板自然填充可用空间,同时保持边距
SettingsContentPanel.HorizontalAlignment = HorizontalAlignment.Stretch;
SettingsContentPanel.VerticalAlignment = VerticalAlignment.Stretch;
SettingsContentPanel.Margin = inset; SettingsContentPanel.Margin = inset;
SettingsContentPanel.Width = double.NaN;
SettingsContentPanel.Height = double.NaN;
} }
private void UpdateWallpaperPreviewLayout() private void UpdateWallpaperPreviewLayout()
{ {
if (WallpaperPreviewFrame is null || if (WallpaperPreviewFrame is null ||
WallpaperPreviewHost is null || WallpaperPreviewHost is null ||
WallpaperPreviewViewport is null ||
WallpaperPreviewGrid is null) WallpaperPreviewGrid is null)
{ {
return; return;
@@ -411,80 +431,65 @@ public partial class MainWindow : Window
_isUpdatingWallpaperPreviewLayout = true; _isUpdatingWallpaperPreviewLayout = true;
try try
{ {
var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width); var desktopWidth = Math.Max(1, DesktopHost.Bounds.Width);
var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height); var desktopHeight = Math.Max(1, DesktopHost.Bounds.Height);
var aspectRatio = desktopWidth / desktopHeight; var aspectRatio = desktopWidth / desktopHeight;
var availableWidth = WallpaperPreviewHost.Bounds.Width - 20;
var availableHeight = WallpaperPreviewHost.Bounds.Height - 20;
if (availableWidth <= 1)
{
availableWidth = WallpaperPreviewFrame.Width;
}
if (availableHeight <= 1)
{
availableHeight = WallpaperPreviewFrame.Height;
}
availableWidth = Math.Max(1, availableWidth);
availableHeight = Math.Max(1, availableHeight);
var previewWidth = Math.Min(availableWidth, WallpaperPreviewMaxWidth); // Use the host width (which is roughly 50% of the settings area)
var previewHeight = previewWidth / aspectRatio; // Subtract padding for the outer host container if needed, but let it stretch
if (previewHeight > availableHeight) var availableWidth = Math.Max(100, WallpaperPreviewHost.Bounds.Width);
{
previewHeight = availableHeight;
previewWidth = previewHeight * aspectRatio;
}
if (Math.Abs(WallpaperPreviewFrame.Width - previewWidth) > 0.5) // Calculate height based on aspect ratio
{ var previewWidth = availableWidth;
var previewHeight = previewWidth / aspectRatio;
// Apply sizes to the monitor frame
WallpaperPreviewFrame.Width = previewWidth; WallpaperPreviewFrame.Width = previewWidth;
}
if (Math.Abs(WallpaperPreviewFrame.Height - previewHeight) > 0.5)
{
WallpaperPreviewFrame.Height = previewHeight; WallpaperPreviewFrame.Height = previewHeight;
}
WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm"); WallpaperPreviewClockTextBlock.Text = DateTime.Now.ToString("HH:mm");
var gridMetrics = CalculateGridMetrics(previewWidth, previewHeight, _targetShortSideCells); var gridMetrics = CalculateGridMetrics(previewWidth, previewHeight, _targetShortSideCells);
if (gridMetrics.CellSize <= 0) if (gridMetrics.CellSize <= 0)
{ {
return; return;
} }
WallpaperPreviewGrid.RowDefinitions.Clear(); WallpaperPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
WallpaperPreviewGrid.ColumnDefinitions.Clear(); WallpaperPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
WallpaperPreviewGrid.Width = gridMetrics.ColumnCount * gridMetrics.CellSize;
WallpaperPreviewGrid.Height = gridMetrics.RowCount * gridMetrics.CellSize;
for (var row = 0; row < gridMetrics.RowCount; row++) // 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.Add( WallpaperPreviewGrid.RowDefinitions.Clear();
new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); WallpaperPreviewGrid.ColumnDefinitions.Clear();
}
for (var col = 0; col < gridMetrics.ColumnCount; col++) for (var row = 0; row < gridMetrics.RowCount; row++)
{ {
WallpaperPreviewGrid.ColumnDefinitions.Add( WallpaperPreviewGrid.RowDefinitions.Add(
new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel))); new RowDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
} }
PlaceStatusBarComponent( for (var col = 0; col < gridMetrics.ColumnCount; col++)
WallpaperPreviewTopStatusBarHost, {
column: 0, WallpaperPreviewGrid.ColumnDefinitions.Add(
requestedColumnSpan: gridMetrics.ColumnCount, new ColumnDefinition(new GridLength(gridMetrics.CellSize, GridUnitType.Pixel)));
totalColumns: gridMetrics.ColumnCount); }
var taskbarRow = gridMetrics.RowCount - 1; PlaceStatusBarComponent(
Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow); WallpaperPreviewTopStatusBarHost,
Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0); column: 0,
Grid.SetRowSpan(WallpaperPreviewBottomTaskbarContainer, 1); requestedColumnSpan: gridMetrics.ColumnCount,
Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount); totalColumns: gridMetrics.ColumnCount);
ApplyTopStatusComponentVisibility(); var taskbarRow = gridMetrics.RowCount - 1;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); Grid.SetRow(WallpaperPreviewBottomTaskbarContainer, taskbarRow);
ApplyPreviewWidgetSizing(gridMetrics.CellSize); Grid.SetColumn(WallpaperPreviewBottomTaskbarContainer, 0);
Grid.SetRowSpan(WallpaperPreviewBottomTaskbarContainer, 1);
Grid.SetColumnSpan(WallpaperPreviewBottomTaskbarContainer, gridMetrics.ColumnCount);
ApplyTopStatusComponentVisibility();
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
ApplyPreviewWidgetSizing(gridMetrics.CellSize);
} }
finally finally
{ {

Binary file not shown.

1
testicon/Program.cs Normal file
View File

@@ -0,0 +1 @@
using System; class Program { static void Main() { foreach (var name in Enum.GetNames(typeof(FluentIcons.Common.Symbol))) Console.WriteLine(name); } }

10
testicon/testicon.csproj Normal file
View File

@@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>