using Avalonia.Media.Imaging; namespace LanMountainDesktop.Launcher.Shell; internal static class LauncherBackgroundService { private const string PictureFileName = "Launcher Picture"; private const long MaxFileSize = 10 * 1024 * 1024; private static readonly string[] SupportedExtensions = [ ".png", ".jpg", ".jpeg", ".bmp", ".gif", ".webp" ]; private static Bitmap? _cachedBitmap; private static string? _cachedPath; private static long _cachedLength; private static DateTime _cachedLastWriteTimeUtc; internal static string? LauncherDataDirectoryOverride { get; set; } public record BackgroundImageInfo { public required bool Exists { get; init; } public required bool IsValid { get; init; } public string? FilePath { get; init; } public Bitmap? Bitmap { get; init; } public int Width { get; init; } public int Height { get; init; } public double AspectRatio { get; init; } public string? ErrorMessage { get; init; } } public record BackgroundImageMutationResult { public required bool IsSuccess { get; init; } public string? FilePath { get; init; } public string? ErrorMessage { get; init; } } public static BackgroundImageInfo LoadBackgroundImage() { try { var launcherPath = ResolveLauncherDataPath(); var imagePath = FindImageFile(launcherPath); if (imagePath is null) { return new BackgroundImageInfo { Exists = false, IsValid = false, ErrorMessage = "No launcher background image was found." }; } var fileInfo = new FileInfo(imagePath); if (fileInfo.Length > MaxFileSize) { return new BackgroundImageInfo { Exists = true, IsValid = false, FilePath = imagePath, ErrorMessage = $"Image file is too large ({fileInfo.Length / 1024 / 1024}MB > 10MB)." }; } if (IsCacheCurrent(imagePath, fileInfo)) { return new BackgroundImageInfo { Exists = true, IsValid = true, FilePath = imagePath, Bitmap = _cachedBitmap, Width = _cachedBitmap!.PixelSize.Width, Height = _cachedBitmap.PixelSize.Height, AspectRatio = (double)_cachedBitmap.PixelSize.Width / _cachedBitmap.PixelSize.Height }; } DisposeCache(); Bitmap bitmap; try { bitmap = new Bitmap(imagePath); } catch (Exception ex) { return new BackgroundImageInfo { Exists = true, IsValid = false, FilePath = imagePath, ErrorMessage = $"Image could not be decoded: {ex.Message}" }; } var width = bitmap.PixelSize.Width; var height = bitmap.PixelSize.Height; var aspectRatio = height == 0 ? 0d : (double)width / height; _cachedBitmap = bitmap; _cachedPath = imagePath; _cachedLength = fileInfo.Length; _cachedLastWriteTimeUtc = fileInfo.LastWriteTimeUtc; Logger.Info($"[LauncherBackground] Background image loaded: {imagePath} ({width}x{height})."); return new BackgroundImageInfo { Exists = true, IsValid = true, FilePath = imagePath, Bitmap = bitmap, Width = width, Height = height, AspectRatio = aspectRatio }; } catch (Exception ex) { Logger.Warn($"[LauncherBackground] Failed to load background image: {ex.Message}"); return new BackgroundImageInfo { Exists = false, IsValid = false, ErrorMessage = $"Load failed: {ex.Message}" }; } } public static BackgroundImageMutationResult SaveBackgroundImage(string sourcePath) { try { if (string.IsNullOrWhiteSpace(sourcePath)) { return FailMutation("No image file was selected."); } var fullSourcePath = Path.GetFullPath(sourcePath); if (!File.Exists(fullSourcePath)) { return FailMutation("The selected image file does not exist."); } var extension = NormalizeExtension(Path.GetExtension(fullSourcePath)); if (!IsSupportedExtension(extension)) { return FailMutation("The selected image format is not supported."); } var sourceInfo = new FileInfo(fullSourcePath); if (sourceInfo.Length > MaxFileSize) { return FailMutation($"Image file is too large ({sourceInfo.Length / 1024 / 1024}MB > 10MB)."); } try { using var bitmap = new Bitmap(fullSourcePath); _ = bitmap.PixelSize; } catch (Exception ex) { return FailMutation($"The selected image could not be decoded: {ex.Message}"); } var launcherPath = ResolveLauncherDataPath(); Directory.CreateDirectory(launcherPath); var destinationPath = Path.Combine(launcherPath, PictureFileName + extension); var tempPath = Path.Combine(launcherPath, $".{PictureFileName}.{Guid.NewGuid():N}.tmp"); try { File.Copy(fullSourcePath, tempPath, overwrite: true); ClearCache(); File.Move(tempPath, destinationPath, overwrite: true); DeleteManagedImageFiles(launcherPath, destinationPath); } finally { TryDeleteFile(tempPath); } ClearCache(); Logger.Info($"[LauncherBackground] Background image saved: {destinationPath}."); return new BackgroundImageMutationResult { IsSuccess = true, FilePath = destinationPath }; } catch (Exception ex) { Logger.Warn($"[LauncherBackground] Failed to save background image: {ex.Message}"); return FailMutation($"Save failed: {ex.Message}"); } } public static BackgroundImageMutationResult ClearBackgroundImage() { try { var launcherPath = ResolveLauncherDataPath(); ClearCache(); DeleteManagedImageFiles(launcherPath, exceptPath: null); Logger.Info("[LauncherBackground] Background image cleared."); return new BackgroundImageMutationResult { IsSuccess = true }; } catch (Exception ex) { Logger.Warn($"[LauncherBackground] Failed to clear background image: {ex.Message}"); return FailMutation($"Clear failed: {ex.Message}"); } } public static void ClearCache() { DisposeCache(); _cachedPath = null; _cachedLength = 0; _cachedLastWriteTimeUtc = DateTime.MinValue; } internal static string? FindManagedImageFile() { return FindImageFile(ResolveLauncherDataPath()); } internal static IReadOnlyList GetSupportedExtensions() => SupportedExtensions; private static BackgroundImageMutationResult FailMutation(string message) { return new BackgroundImageMutationResult { IsSuccess = false, ErrorMessage = message }; } private static bool IsCacheCurrent(string imagePath, FileInfo fileInfo) { return _cachedBitmap is not null && string.Equals(_cachedPath, imagePath, StringComparison.OrdinalIgnoreCase) && _cachedLength == fileInfo.Length && _cachedLastWriteTimeUtc == fileInfo.LastWriteTimeUtc; } private static string? FindImageFile(string directory) { if (!Directory.Exists(directory)) { return null; } foreach (var extension in SupportedExtensions) { var path = Path.Combine(directory, PictureFileName + extension); if (File.Exists(path)) { return path; } } foreach (var file in Directory.GetFiles(directory, PictureFileName + ".*")) { if (IsSupportedExtension(Path.GetExtension(file))) { return file; } } return null; } private static void DeleteManagedImageFiles(string directory, string? exceptPath) { if (!Directory.Exists(directory)) { return; } foreach (var file in Directory.GetFiles(directory, PictureFileName + ".*")) { if (!IsSupportedExtension(Path.GetExtension(file))) { continue; } if (!string.IsNullOrWhiteSpace(exceptPath) && string.Equals(Path.GetFullPath(file), Path.GetFullPath(exceptPath), StringComparison.OrdinalIgnoreCase)) { continue; } TryDeleteFile(file); } } private static void TryDeleteFile(string path) { try { if (File.Exists(path)) { File.Delete(path); } } catch (Exception ex) { Logger.Warn($"[LauncherBackground] Failed to delete '{path}': {ex.Message}"); } } private static string NormalizeExtension(string? extension) { return string.IsNullOrWhiteSpace(extension) ? string.Empty : extension.Trim().ToLowerInvariant(); } private static bool IsSupportedExtension(string? extension) { var normalized = NormalizeExtension(extension); return SupportedExtensions.Contains(normalized, StringComparer.OrdinalIgnoreCase); } private static string ResolveLauncherDataPath() { if (!string.IsNullOrWhiteSpace(LauncherDataDirectoryOverride)) { return Path.GetFullPath(LauncherDataDirectoryOverride); } var resolver = new DataLocationResolver(AppContext.BaseDirectory); return resolver.ResolveLauncherDataPath(); } private static void DisposeCache() { _cachedBitmap?.Dispose(); _cachedBitmap = null; } }