Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 149 additions & 1 deletion Wauncher/Services/CarouselService.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
Expand Down Expand Up @@ -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<string, Bitmap> _imageCache = new();
private readonly ConcurrentDictionary<string, Task<Bitmap?>> _loadingTasks = new();
private readonly SemaphoreSlim _cacheSemaphore = new(1, 1);

public bool IsOfflineMode => !System.Net.NetworkInformation.NetworkInterface.GetIsNetworkAvailable();

public async Task SetupCarouselAsync()
Expand Down Expand Up @@ -75,8 +83,148 @@ public async Task SetupCarouselAsync()

public async Task TeardownCarouselAsync()
{
await ClearImageCacheAsync();
await Task.CompletedTask;
}

// Lazy loading implementation
public async Task<Bitmap?> 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<string> urls)
{
if (urls == null || urls.Count == 0)
return;

var preloadTasks = new List<Task>();

// 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<string> 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<string>();

// 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<Bitmap?> 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)
{
Expand Down
7 changes: 7 additions & 0 deletions Wauncher/Services/ICarouselService.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;

namespace Wauncher.Services
{
Expand All @@ -9,5 +10,11 @@ public interface ICarouselService
Task<List<string>?> LoadCarouselUrlsFromGitHubAsync();
Task TeardownCarouselAsync();
bool IsOfflineMode { get; }

// Lazy loading methods
Task<Bitmap?> LoadImageAsync(int index, string url);
Task PreloadAdjacentImagesAsync(int currentIndex, List<string> urls);
Task UnloadDistantImagesAsync(int currentIndex, List<string> urls, int maxCachedCount = 3);
void ClearImageCache();
}
}
Loading
Loading