From e0d7ffd674414ce3d3df7f2176343c2f3d8229a4 Mon Sep 17 00:00:00 2001 From: Ioannis Date: Mon, 6 Apr 2026 13:00:11 +0100 Subject: [PATCH 1/3] Add lazy loading methods to ICarouselService Added lazy loading methods for image handling in carousel service. --- Wauncher/Services/ICarouselService.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Wauncher/Services/ICarouselService.cs b/Wauncher/Services/ICarouselService.cs index 64ddf8b..059193f 100644 --- a/Wauncher/Services/ICarouselService.cs +++ b/Wauncher/Services/ICarouselService.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Media.Imaging; namespace Wauncher.Services { @@ -9,5 +10,11 @@ public interface ICarouselService Task?> LoadCarouselUrlsFromGitHubAsync(); Task TeardownCarouselAsync(); bool IsOfflineMode { get; } + + // Lazy loading methods + Task LoadImageAsync(int index, string url); + Task PreloadAdjacentImagesAsync(int currentIndex, List urls); + Task UnloadDistantImagesAsync(int currentIndex, List urls, int maxCachedCount = 3); + void ClearImageCache(); } } From 0af6802129bc06a1687e982b1181499615e3ce04 Mon Sep 17 00:00:00 2001 From: Ioannis Date: Mon, 6 Apr 2026 13:01:07 +0100 Subject: [PATCH 2/3] Refactor using directives and code structure --- Wauncher/Views/MainWindow.axaml.cs | 600 +++++++++++++++-------------- 1 file changed, 313 insertions(+), 287 deletions(-) diff --git a/Wauncher/Views/MainWindow.axaml.cs b/Wauncher/Views/MainWindow.axaml.cs index 8ecdd4a..5f5c1e9 100644 --- a/Wauncher/Views/MainWindow.axaml.cs +++ b/Wauncher/Views/MainWindow.axaml.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.IO; +using System.Linq; using System.Net.Http; using System.Net.NetworkInformation; using System.Security.Cryptography; @@ -12,17 +13,17 @@ using Avalonia.Animation.Easings; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; -using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Media; -using Avalonia.Media.Imaging; -using Avalonia.Threading; -using System.ComponentModel; -using System.Diagnostics; -using SkiaSharp; -using Wauncher.Services; -using Wauncher.ViewModels; -using Wauncher.Utils; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Media; +using Avalonia.Media.Imaging; +using Avalonia.Threading; +using System.ComponentModel; +using System.Diagnostics; +using SkiaSharp; +using Wauncher.Services; +using Wauncher.ViewModels; +using Wauncher.Utils; namespace Wauncher.Views { @@ -44,24 +45,24 @@ public partial class MainWindow : Window private ICarouselService? _carouselService; private readonly List _zoomCts = new(); - private bool _forceClose; - private bool _isLoaded; - private int _carouselInitInProgress; - private Image[] _carouselImages = Array.Empty(); - private List _carouselImageUrls = new(); + private bool _forceClose; + private bool _isLoaded; + private int _carouselInitInProgress; + private Image[] _carouselImages = Array.Empty(); + private List _carouselImageUrls = new(); private DispatcherTimer? _carouselTimer; private int _currentCarouselIndex; private int _currentCarouselSlot; private int _carouselRotateInProgress; - public MainWindow() - { - InitializeComponent(); - SettingsWindowViewModel.DisableCarouselChanged += OnDisableCarouselChanged; - - // Initialize services in background to improve startup performance - _ = Task.Run(() => - { + public MainWindow() + { + InitializeComponent(); + SettingsWindowViewModel.DisableCarouselChanged += OnDisableCarouselChanged; + + // Initialize services in background to improve startup performance + _ = Task.Run(() => + { try { ServiceContainer.Initialize(); @@ -75,15 +76,15 @@ public MainWindow() ServiceContainer.GetService(), ServiceContainer.GetService()); - Dispatcher.UIThread.Post(() => - { - DataContext = viewModel; - viewModel.PropertyChanged += ViewModel_PropertyChanged; - if (_isLoaded) - _ = InitializeCarouselAsync(); - }); - } - catch (Exception ex) + Dispatcher.UIThread.Post(() => + { + DataContext = viewModel; + viewModel.PropertyChanged += ViewModel_PropertyChanged; + if (_isLoaded) + _ = InitializeCarouselAsync(); + }); + } + catch (Exception ex) { Dispatcher.UIThread.Post(() => { @@ -92,113 +93,115 @@ public MainWindow() } }); - Loaded += (_, _) => - { - _isLoaded = true; - if (_carouselService != null) - _ = InitializeCarouselAsync(); - _ = PatchNotesControl.LoadPatchNotesAsync(); - }; - - Closing += (_, e) => - { - if (_forceClose) - return; - - var settings = SettingsWindowViewModel.LoadGlobal(); - bool shouldHideToTray = - settings.MinimizeToTray && - (Game.IsRunning() || - string.Equals((DataContext as MainWindowViewModel)?.GameStatus, "Running", StringComparison.OrdinalIgnoreCase)); - - if (shouldHideToTray) - { - e.Cancel = true; - Hide(); - MemoryManager.StartBackgroundCleanup(); - return; - } - - MemoryManager.StopBackgroundCleanup(); - - try - { - if (Application.Current is App app) - { - var trayIconField = typeof(App).GetField("_trayIcon", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var trayIcon = trayIconField?.GetValue(app) as Avalonia.Controls.TrayIcon; - trayIcon?.Dispose(); - } - } - catch - { - } - - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - { - _forceClose = true; - Dispatcher.UIThread.Post(() => desktop.Shutdown()); - } - }; - - Closed += (_, _) => _ = CleanupServicesAsync(); - } - - private async Task InitializeCarouselAsync() - { - if (Interlocked.Exchange(ref _carouselInitInProgress, 1) == 1) - return; - - try - { - for (int attempt = 0; attempt < 20 && _carouselService == null; attempt++) - await Task.Delay(100); - - if (_carouselService == null) - return; - - TeardownCarousel(); - - var carouselContainer = this.FindControl("CarouselContainer"); - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineTitle = this.FindControl("CarouselOfflineTitle"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - if (carouselContainer == null) - return; - - var settings = SettingsWindowViewModel.LoadGlobal(); - if (settings.DisableCarousel) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - - if (offlineTitle != null) - offlineTitle.Text = "Carousel Disabled"; - - if (offlineSubText != null) - offlineSubText.Text = "Carousel is turned off in settings."; - - return; - } - - bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); - var urls = hasInternet - ? await _carouselService.LoadCarouselUrlsFromGitHubAsync() - : null; - - if (urls == null || urls.Count == 0) - { - if (offlinePanel != null) - offlinePanel.IsVisible = true; - - if (offlineTitle != null) - offlineTitle.Text = "No internet connection"; - - if (offlineSubText != null) - { - offlineSubText.Text = hasInternet - ? "Carousel is temporarily unavailable." + Loaded += (_, _) => + { + _isLoaded = true; + if (_carouselService != null) + _ = InitializeCarouselAsync(); + _ = PatchNotesControl.LoadPatchNotesAsync(); + }; + + Closing += (_, e) => + { + if (_forceClose) + return; + + var settings = SettingsWindowViewModel.LoadGlobal(); + bool shouldHideToTray = + settings.MinimizeToTray && + (Game.IsRunning() || + string.Equals((DataContext as MainWindowViewModel)?.GameStatus, "Running", StringComparison.OrdinalIgnoreCase)); + + if (shouldHideToTray) + { + e.Cancel = true; + Hide(); + MemoryManager.StartBackgroundCleanup(); + return; + } + + MemoryManager.StopBackgroundCleanup(); + + try + { + if (Application.Current is App app) + { + var trayIconField = typeof(App).GetField("_trayIcon", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var trayIcon = trayIconField?.GetValue(app) as Avalonia.Controls.TrayIcon; + trayIcon?.Dispose(); + } + } + catch + { + } + + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + _forceClose = true; + Dispatcher.UIThread.Post(() => desktop.Shutdown()); + } + }; + + Closed += (_, _) => _ = CleanupServicesAsync(); + } + + private async Task InitializeCarouselAsync() + { + if (Interlocked.Exchange(ref _carouselInitInProgress, 1) == 1) + return; + + try + { + for (int attempt = 0; attempt < 20 && _carouselService == null; attempt++) + await Task.Delay(100); + + if (_carouselService == null) + return; + + TeardownCarousel(); + + var carouselContainer = this.FindControl("CarouselContainer"); + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineTitle = this.FindControl("CarouselOfflineTitle"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + if (carouselContainer == null) + return; + + var settings = SettingsWindowViewModel.LoadGlobal(); + if (settings.DisableCarousel) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + + if (offlineTitle != null) + offlineTitle.Text = "Carousel Disabled"; + + if (offlineSubText != null) + offlineSubText.Text = "Carousel is turned off in settings."; + + return; + } + + bool hasInternet = NetworkInterface.GetIsNetworkAvailable(); + var urls = hasInternet + ? await _carouselService.LoadCarouselUrlsFromGitHubAsync() + : null; + + if (urls == null || urls.Count == 0) + { + if (offlinePanel != null) + offlinePanel.IsVisible = true; + + if (offlineTitle != null) + offlineTitle.Text = hasInternet + ? "Carousel Unavailable" + : "Offline Mode"; + + if (offlineSubText != null) + { + offlineSubText.Text = hasInternet + ? "Carousel is temporarily unavailable." : "Connect to Wi-Fi or Ethernet to load the carousel."; } @@ -229,36 +232,38 @@ private async Task InitializeCarouselAsync() _currentCarouselIndex = 0; _currentCarouselSlot = 0; - await SetCarouselImageAsync(_carouselImages[_currentCarouselSlot], _carouselImageUrls[_currentCarouselIndex]); + await SetCarouselImageLazyAsync(_carouselImages[_currentCarouselSlot], _currentCarouselIndex, _carouselImageUrls[_currentCarouselIndex]); _carouselImages[_currentCarouselSlot].Opacity = 1.0; StartZoomOut(_carouselImages[_currentCarouselSlot], _currentCarouselSlot); + await _carouselService.PreloadAdjacentImagesAsync(_currentCarouselIndex, _carouselImageUrls); + _carouselTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(CarouselRotationIntervalSeconds) }; _carouselTimer.Tick += async (_, _) => await RotateCarouselAsync(); _carouselTimer.Start(); } - catch (Exception ex) - { - System.Diagnostics.Debug.WriteLine("Carousel init failed: " + ex.Message); - } - finally - { - Interlocked.Exchange(ref _carouselInitInProgress, 0); - } - } - - private async Task CleanupServicesAsync() - { - SettingsWindowViewModel.DisableCarouselChanged -= OnDisableCarouselChanged; - TeardownCarousel(); - - try - { - if (_carouselService != null) - await _carouselService.TeardownCarouselAsync(); - } - catch - { + catch (Exception ex) + { + System.Diagnostics.Debug.WriteLine("Carousel init failed: " + ex.Message); + } + finally + { + Interlocked.Exchange(ref _carouselInitInProgress, 0); + } + } + + private async Task CleanupServicesAsync() + { + SettingsWindowViewModel.DisableCarouselChanged -= OnDisableCarouselChanged; + TeardownCarousel(); + + try + { + if (_carouselService != null) + await _carouselService.TeardownCarouselAsync(); + } + catch + { } } @@ -272,36 +277,36 @@ private void ViewModel_PropertyChanged(object? sender, PropertyChangedEventArgs var settings = SettingsWindowViewModel.LoadGlobal(); - if (!_forceClose && - settings.MinimizeToTray && - string.Equals(vm.GameStatus, "Running", StringComparison.OrdinalIgnoreCase)) - { - Dispatcher.UIThread.Post(() => - { - if (IsVisible) - Hide(); - }); - MemoryManager.StartBackgroundCleanup(); - return; - } - - if (!_forceClose && - settings.MinimizeToTray && - string.Equals(vm.GameStatus, "Not Running", StringComparison.OrdinalIgnoreCase) && - !IsVisible) - { - Dispatcher.UIThread.Post(() => - { - Show(); - WindowState = WindowState.Normal; - Activate(); - }); - MemoryManager.StopBackgroundCleanup(); - return; - } - - if (string.Equals(vm.GameStatus, "Not Running", StringComparison.OrdinalIgnoreCase)) - MemoryManager.StopBackgroundCleanup(); + if (!_forceClose && + settings.MinimizeToTray && + string.Equals(vm.GameStatus, "Running", StringComparison.OrdinalIgnoreCase)) + { + Dispatcher.UIThread.Post(() => + { + if (IsVisible) + Hide(); + }); + MemoryManager.StartBackgroundCleanup(); + return; + } + + if (!_forceClose && + settings.MinimizeToTray && + string.Equals(vm.GameStatus, "Not Running", StringComparison.OrdinalIgnoreCase) && + !IsVisible) + { + Dispatcher.UIThread.Post(() => + { + Show(); + WindowState = WindowState.Normal; + Activate(); + }); + MemoryManager.StopBackgroundCleanup(); + return; + } + + if (string.Equals(vm.GameStatus, "Not Running", StringComparison.OrdinalIgnoreCase)) + MemoryManager.StopBackgroundCleanup(); } private static Image[] CreateCarouselImages(int count) @@ -348,7 +353,7 @@ private async Task RotateCarouselAsync() int nextSlot = (_currentCarouselSlot + 1) % _carouselImages.Length; int currentSlot = _currentCarouselSlot; - await SetCarouselImageAsync(_carouselImages[nextSlot], _carouselImageUrls[nextIndex]); + await SetCarouselImageLazyAsync(_carouselImages[nextSlot], nextIndex, _carouselImageUrls[nextIndex]); _carouselImages[currentSlot].Opacity = 0.0; StartZoomOut(_carouselImages[nextSlot], nextSlot); @@ -356,6 +361,13 @@ private async Task RotateCarouselAsync() _currentCarouselIndex = nextIndex; _currentCarouselSlot = nextSlot; + + // Preload next adjacent images and unload distant ones + if (_carouselService != null) + { + await _carouselService.PreloadAdjacentImagesAsync(_currentCarouselIndex, _carouselImageUrls); + await _carouselService.UnloadDistantImagesAsync(_currentCarouselIndex, _carouselImageUrls); + } } finally { @@ -386,39 +398,53 @@ private void TeardownCarousel() _carouselImages = Array.Empty(); _currentCarouselIndex = 0; _currentCarouselSlot = 0; - Interlocked.Exchange(ref _carouselRotateInProgress, 0); - } - - private void OnDisableCarouselChanged(bool disabled) - { - Dispatcher.UIThread.Post(async () => - { - var offlinePanel = this.FindControl("CarouselOfflinePanel"); - var offlineTitle = this.FindControl("CarouselOfflineTitle"); - var offlineSubText = this.FindControl("CarouselOfflineSubText"); - - if (disabled) - { - TeardownCarousel(); - - if (offlinePanel != null) - offlinePanel.IsVisible = true; - - if (offlineTitle != null) - offlineTitle.Text = "Carousel Disabled"; - - if (offlineSubText != null) - offlineSubText.Text = "Carousel is turned off in settings."; - - return; - } - - if (offlinePanel != null) - offlinePanel.IsVisible = false; - - await InitializeCarouselAsync(); - }); - } + Interlocked.Exchange(ref _carouselRotateInProgress, 0); + + _carouselService?.ClearImageCache(); + } + + private void OnDisableCarouselChanged(bool disabled) + { + Dispatcher.UIThread.Post(async () => + { + var offlinePanel = this.FindControl("CarouselOfflinePanel"); + var offlineTitle = this.FindControl("CarouselOfflineTitle"); + var offlineSubText = this.FindControl("CarouselOfflineSubText"); + + if (disabled) + { + TeardownCarousel(); + + if (offlinePanel != null) + offlinePanel.IsVisible = true; + + if (offlineTitle != null) + offlineTitle.Text = "Carousel Disabled"; + + if (offlineSubText != null) + offlineSubText.Text = "Carousel is turned off in settings."; + + return; + } + + if (offlinePanel != null) + offlinePanel.IsVisible = false; + + await InitializeCarouselAsync(); + }); + } + + private async Task SetCarouselImageLazyAsync(Image image, int index, string url) + { + var nextBitmap = await _carouselService?.LoadImageAsync(index, url); + if (nextBitmap == null) + return; + + if (image.Source is IDisposable disposable) + disposable.Dispose(); + + image.Source = nextBitmap; + } private async Task SetCarouselImageAsync(Image image, string url) { @@ -582,65 +608,65 @@ private void MinimizeButton_Click(object? sender, RoutedEventArgs e) WindowState = WindowState.Minimized; } - private void CloseButton_Click(object? sender, RoutedEventArgs e) - { - ForceQuit(); - } - - public void ForceQuit() - { - _forceClose = true; - - try - { - TeardownCarousel(); - } - catch - { - } - - MemoryManager.StopBackgroundCleanup(); - - try - { - if (Application.Current is App app) - { - var trayIconField = typeof(App).GetField("_trayIcon", - System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); - var trayIcon = trayIconField?.GetValue(app) as Avalonia.Controls.TrayIcon; - trayIcon?.Dispose(); - } - } - catch - { - } - - try - { - Close(); - } - catch - { - } - - try - { - if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) - desktop.Shutdown(); - } - catch - { - } - - Environment.Exit(0); - - try - { - Process.GetCurrentProcess().Kill(); - } - catch - { - } - } + private void CloseButton_Click(object? sender, RoutedEventArgs e) + { + ForceQuit(); + } + + public void ForceQuit() + { + _forceClose = true; + + try + { + TeardownCarousel(); + } + catch + { + } + + MemoryManager.StopBackgroundCleanup(); + + try + { + if (Application.Current is App app) + { + var trayIconField = typeof(App).GetField("_trayIcon", + System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance); + var trayIcon = trayIconField?.GetValue(app) as Avalonia.Controls.TrayIcon; + trayIcon?.Dispose(); + } + } + catch + { + } + + try + { + Close(); + } + catch + { + } + + try + { + if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + desktop.Shutdown(); + } + catch + { + } + + Environment.Exit(0); + + try + { + Process.GetCurrentProcess().Kill(); + } + catch + { + } + } } } From 34c767752b9c49ae330455961690477c415e77cf Mon Sep 17 00:00:00 2001 From: Ioannis Date: Mon, 6 Apr 2026 13:01:51 +0100 Subject: [PATCH 3/3] Implement lazy loading and image caching Added lazy loading and caching for carousel images. --- Wauncher/Services/CarouselService.cs | 150 ++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 1 deletion(-) diff --git a/Wauncher/Services/CarouselService.cs b/Wauncher/Services/CarouselService.cs index 03afd93..6ac51de 100644 --- a/Wauncher/Services/CarouselService.cs +++ b/Wauncher/Services/CarouselService.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; @@ -32,7 +33,14 @@ public class CarouselService : ICarouselService private const int CarouselMaxWidth = 1280; private const int CarouselMaxHeight = 720; - + private const int MaxCachedImages = 3; // Keep current + adjacent images + private const int PreloadDistance = 1; // Preload 1 image before/after current + + // Lazy loading cache + private readonly ConcurrentDictionary _imageCache = new(); + private readonly ConcurrentDictionary> _loadingTasks = new(); + private readonly SemaphoreSlim _cacheSemaphore = new(1, 1); + public bool IsOfflineMode => !System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable(); public async Task SetupCarouselAsync() @@ -75,8 +83,148 @@ public async Task SetupCarouselAsync() public async Task TeardownCarouselAsync() { + await ClearImageCacheAsync(); await Task.CompletedTask; } + + // Lazy loading implementation + public async Task LoadImageAsync(int index, string url) + { + if (string.IsNullOrEmpty(url)) + return null; + + // Check cache first + if (_imageCache.TryGetValue(url, out var cachedBitmap)) + return cachedBitmap; + + // Check if already loading + var loadingTask = _loadingTasks.GetOrAdd(url, async (key) => await LoadImageInternalAsync(key)); + + try + { + var bitmap = await loadingTask; + if (bitmap != null) + { + _imageCache.TryAdd(url, bitmap); + } + return bitmap; + } + finally + { + _loadingTasks.TryRemove(url, out _); + } + } + + public async Task PreloadAdjacentImagesAsync(int currentIndex, List urls) + { + if (urls == null || urls.Count == 0) + return; + + var preloadTasks = new List(); + + // Preload previous image + var prevIndex = (currentIndex - 1 + urls.Count) % urls.Count; + if (prevIndex != currentIndex) + { + preloadTasks.Add(LoadImageAsync(prevIndex, urls[prevIndex])); + } + + // Preload next image + var nextIndex = (currentIndex + 1) % urls.Count; + if (nextIndex != currentIndex && nextIndex != prevIndex) + { + preloadTasks.Add(LoadImageAsync(nextIndex, urls[nextIndex])); + } + + // Execute preloads in parallel + await Task.WhenAll(preloadTasks); + } + + public async Task UnloadDistantImagesAsync(int currentIndex, List urls, int maxCachedCount = MaxCachedImages) + { + if (urls == null || urls.Count == 0) + return; + + await _cacheSemaphore.WaitAsync(); + try + { + // Determine which URLs to keep (current + adjacent) + var urlsToKeep = new HashSet(); + + // Keep current + if (currentIndex >= 0 && currentIndex < urls.Count) + urlsToKeep.Add(urls[currentIndex]); + + // Keep adjacent + for (int i = -PreloadDistance; i <= PreloadDistance; i++) + { + var index = (currentIndex + i + urls.Count) % urls.Count; + if (index >= 0 && index < urls.Count) + urlsToKeep.Add(urls[index]); + } + + // Remove distant images from cache + var keysToRemove = _imageCache.Keys + .Where(url => !urlsToKeep.Contains(url)) + .ToList(); + + foreach (var key in keysToRemove) + { + if (_imageCache.TryRemove(key, out var bitmap)) + { + bitmap?.Dispose(); + } + } + } + finally + { + _cacheSemaphore.Release(); + } + } + + public void ClearImageCache() + { + _ = ClearImageCacheAsync(); + } + + private async Task ClearImageCacheAsync() + { + await _cacheSemaphore.WaitAsync(); + try + { + foreach (var kvp in _imageCache) + { + kvp.Value?.Dispose(); + } + _imageCache.Clear(); + _loadingTasks.Clear(); + } + finally + { + _cacheSemaphore.Release(); + } + } + + private async Task LoadImageInternalAsync(string url) + { + try + { + var cachedBytes = await TryGetCachedCarouselBytesAsync(url); + var bytes = cachedBytes ?? await _http.GetByteArrayAsync(url); + var resized = cachedBytes ?? TryResizeCarouselBytes(bytes) ?? bytes; + + if (cachedBytes == null) + await TryWriteCarouselCacheAsync(url, resized); + + await using var ms = new MemoryStream(resized); + return new Bitmap(ms); + } + catch (Exception ex) + { + ErrorLogger.LogError("CarouselService.LoadImageInternalAsync", ex, $"Failed to load image from URL: {url}"); + return null; + } + } private static int GetCarouselSortIndex(string name) {