using System; using System.Collections.Generic; using System.Globalization; using System.IO; using Avalonia.Data.Converters; using Avalonia.Media.Imaging; namespace LANCommander.Launcher.Converters; /// /// Converts a file path string to a thumbnail Bitmap. /// Decodes at a fixed width so full-resolution source images don't consume /// excessive memory, and caches bitmaps via WeakReference so they are freed /// by the GC once no control is displaying them (e.g. after virtualization /// scrolls them out of view) and reloaded on next access. /// public class FilePathToBitmapConverter : IValueConverter { // Covers are displayed at 140–220 px wide. 320 px gives enough headroom // for high-DPI screens while keeping per-image memory around 400 KB // instead of several MB for full-resolution sources. private const int DecodeWidth = 320; // WeakReference cache: keeps bitmaps alive while on-screen, lets the GC // collect them once nothing holds a strong reference (i.e. off-screen). // Key is "{path}|{decodeWidth}" so full-res and thumbnail don't collide. private static readonly Dictionary> _cache = new(); public static readonly FilePathToBitmapConverter Instance = new(); public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { if (value is not string path || string.IsNullOrEmpty(path)) return null; // ConverterParameter="full" → full resolution // ConverterParameter="h{N}" → DecodeToHeight(N) // ConverterParameter="w{N}" → DecodeToWidth(N) (e.g. game detail background) // default → DecodeToWidth(320) bool fullRes = parameter is string p && p == "full"; int? decodeHeight = parameter is string hp && hp.StartsWith("h") && int.TryParse(hp.AsSpan(1), out var h) ? h : null; int? decodeWidth = parameter is string wp && wp.StartsWith("w") && int.TryParse(wp.AsSpan(1), out var w) ? w : null; var cacheKey = fullRes ? $"{path}|full" : decodeHeight.HasValue ? $"{path}|h{decodeHeight}" : decodeWidth.HasValue ? $"{path}|w{decodeWidth}" : $"{path}|{DecodeWidth}"; if (_cache.TryGetValue(cacheKey, out var weakRef) && weakRef.TryGetTarget(out var cached)) return cached; try { if (!File.Exists(path)) return null; using var stream = File.OpenRead(path); var bitmap = fullRes ? new Bitmap(stream) : decodeHeight.HasValue ? Bitmap.DecodeToHeight(stream, decodeHeight.Value, BitmapInterpolationMode.HighQuality) : Bitmap.DecodeToWidth(stream, decodeWidth ?? DecodeWidth, BitmapInterpolationMode.HighQuality); _cache[cacheKey] = new WeakReference(bitmap); return bitmap; } catch { return null; } } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) => throw new NotSupportedException(); }