using System.Text.Json; using System.Windows.Media; using System.Windows.Media.Imaging; using CommunityToolkit.Mvvm.ComponentModel; using XFEToolBox.Client.Models; using XFEToolBox.Client.Profiles.CacheProfiles; using XFEToolBox.Client.Utilities; namespace XFEToolBox.Client.ViewModel.Pages; public partial class RecentUsageCardViewModel : ObservableObject { private static readonly object CatalogIconSyncRoot = new(); private static string? cachedToolCatalogJson; private static string? cachedSoftwareCatalogJson; private static IReadOnlyDictionary cachedToolIcons = new Dictionary(StringComparer.OrdinalIgnoreCase); private static IReadOnlyDictionary cachedSoftwareIcons = new Dictionary(StringComparer.OrdinalIgnoreCase); public RecentUsageCardViewModel(RecentUsageEntry entry) { Entry = entry; if (RecentUsageIconCache.TryGet(entry.Kind, entry.TargetId, out var cachedIcon)) { IconSource = cachedIcon; } else { IconSource = CreateFallbackIcon(entry); _ = LoadConfiguredIconAsync(entry); } } public RecentUsageEntry Entry { get; } public string Name => Entry.Name; public string Description => Entry.Description; public string Detail => Entry.Detail; public string KindText => Entry.Kind switch { RecentUsageKind.Tool => "工具", RecentUsageKind.Software => "软件", RecentUsageKind.Project => "项目", _ => "功能" }; public string LastUsedText => FormatLastUsed(Entry.LastUsedAtUtc); [ObservableProperty] private ImageSource iconSource; [ObservableProperty] private bool isEnabled = true; private async Task LoadConfiguredIconAsync(RecentUsageEntry entry) { try { var reference = string.IsNullOrWhiteSpace(entry.IconReference) ? await Task.Run(() => ResolveCatalogIcon(entry)) : entry.IconReference; if (string.IsNullOrWhiteSpace(reference) || reference.StartsWith('/')) return; var image = await WebImageSourceLoader.LoadAsync(reference); if (image is not null) { RecentUsageIconCache.Remember(entry.Kind, entry.TargetId, image); IconSource = image; } } catch { // 网络或图标格式不可用时保留对应类型的内置图标。 } } private static ImageSource CreateFallbackIcon(RecentUsageEntry entry) { if (!string.IsNullOrWhiteSpace(entry.IconReference) && entry.IconReference.StartsWith('/')) return CreateResourceImage(entry.IconReference); return CreateResourceImage(entry.Kind switch { RecentUsageKind.Tool => "/Resources/Image/default_tool_icon.png", RecentUsageKind.Software => GetBundledSoftwareIcon(entry.TargetId), RecentUsageKind.Project => "/Resources/Image/wrench.png", _ => "/Resources/Image/grid.png" }); } private static string ResolveCatalogIcon(RecentUsageEntry entry) { var toolCatalogJson = AppCacheProfile.ToolCatalogJson; var softwareCatalogJson = AppCacheProfile.SoftwareCatalogJson; lock (CatalogIconSyncRoot) { if (!ReferenceEquals(cachedToolCatalogJson, toolCatalogJson)) { cachedToolCatalogJson = toolCatalogJson; cachedToolIcons = BuildIconIndex(toolCatalogJson, isToolCatalog: true); } if (!ReferenceEquals(cachedSoftwareCatalogJson, softwareCatalogJson)) { cachedSoftwareCatalogJson = softwareCatalogJson; cachedSoftwareIcons = BuildIconIndex(softwareCatalogJson, isToolCatalog: false); } var icons = entry.Kind == RecentUsageKind.Tool ? cachedToolIcons : cachedSoftwareIcons; return icons.TryGetValue(entry.TargetId, out var iconReference) ? iconReference : string.Empty; } } private static IReadOnlyDictionary BuildIconIndex(string? json, bool isToolCatalog) { if (string.IsNullOrWhiteSpace(json)) return new Dictionary(StringComparer.OrdinalIgnoreCase); try { using var document = JsonDocument.Parse(json); var items = isToolCatalog ? document.RootElement : document.RootElement.TryGetProperty("items", out var softwareItems) ? softwareItems : default; if (items.ValueKind != JsonValueKind.Array) return new Dictionary(StringComparer.OrdinalIgnoreCase); var result = new Dictionary(StringComparer.OrdinalIgnoreCase); var iconPropertyName = isToolCatalog ? "iconDataUrl" : "iconUrl"; foreach (var item in items.EnumerateArray()) { if (!item.TryGetProperty("id", out var idProperty) || !item.TryGetProperty(iconPropertyName, out var iconProperty)) continue; var id = idProperty.GetString(); var icon = iconProperty.GetString(); if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(icon)) result[id] = icon; } return result; } catch (JsonException) { return new Dictionary(StringComparer.OrdinalIgnoreCase); } } private static string GetBundledSoftwareIcon(string id) => id.ToLowerInvariant() switch { "steam" => "/Resources/Image/DownloadImage/steam_logo.png", "watt-toolkit" or "steampp" => "/Resources/Image/DownloadImage/steampp.png", "visual-studio" => "/Resources/Image/DownloadImage/visual_studio.png", "cheat-engine" => "/Resources/Image/DownloadImage/cheat_engine.png", _ => "/Resources/Image/download.png" }; private static ImageSource CreateResourceImage(string resourcePath) { var image = new BitmapImage(new Uri($"pack://application:,,,{resourcePath}", UriKind.Absolute)); image.Freeze(); return image; } private static string FormatLastUsed(DateTimeOffset value) { var elapsed = DateTimeOffset.UtcNow - value; if (elapsed < TimeSpan.FromMinutes(1)) return "刚刚"; if (elapsed < TimeSpan.FromHours(1)) return $"{Math.Max(1, (int)elapsed.TotalMinutes)} 分钟前"; if (elapsed < TimeSpan.FromDays(1)) return $"{Math.Max(1, (int)elapsed.TotalHours)} 小时前"; if (elapsed < TimeSpan.FromDays(7)) return $"{Math.Max(1, (int)elapsed.TotalDays)} 天前"; return value.ToLocalTime().ToString("M 月 d 日"); } }