mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-23 18:04:26 +08:00
0.7.9.2
自习设置,优化设置选项卡图标,加入智教hub组件
This commit is contained in:
@@ -68,6 +68,19 @@ public sealed record ZhiJiaoHubSnapshot(
|
||||
int CurrentIndex,
|
||||
string Source);
|
||||
|
||||
public sealed record ZhiJiaoHubHybridImageItem(
|
||||
string Name,
|
||||
string RemoteUrl,
|
||||
string? LocalPath,
|
||||
int Index,
|
||||
bool IsCached);
|
||||
|
||||
public sealed record ZhiJiaoHubHybridSnapshot(
|
||||
IReadOnlyList<ZhiJiaoHubHybridImageItem> Images,
|
||||
string Source,
|
||||
int CachedCount,
|
||||
int TotalCount);
|
||||
|
||||
public sealed record RecommendationQueryResult<T>(
|
||||
bool Success,
|
||||
T? Data,
|
||||
@@ -353,6 +366,24 @@ public interface IRecommendationInfoService
|
||||
ZhiJiaoHubQuery query,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<RecommendationQueryResult<ZhiJiaoHubHybridSnapshot>> GetZhiJiaoHubHybridImagesAsync(
|
||||
string source,
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<string?> DownloadAndCacheImageAsync(
|
||||
string source,
|
||||
ZhiJiaoHubImageItem image,
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task StartBackgroundDownloadAsync(
|
||||
string source,
|
||||
IReadOnlyList<ZhiJiaoHubHybridImageItem> images,
|
||||
string mirrorSource,
|
||||
Action<int, int, string>? onProgress = null,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
Task<ZhiJiaoHubSyncResult> SyncZhiJiaoHubImagesAsync(
|
||||
string source,
|
||||
string mirrorSource,
|
||||
|
||||
@@ -3456,4 +3456,118 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
|
||||
return _zhiJiaoHubCacheService.HasLocalCache(normalizedSource);
|
||||
}
|
||||
|
||||
public async Task<RecommendationQueryResult<ZhiJiaoHubHybridSnapshot>> GetZhiJiaoHubHybridImagesAsync(
|
||||
string source,
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
|
||||
var normalizedMirror = ZhiJiaoHubMirrorSources.Normalize(mirrorSource);
|
||||
|
||||
var localPathMap = _zhiJiaoHubCacheService.LoadLocalPathMap(normalizedSource);
|
||||
|
||||
try
|
||||
{
|
||||
var query = new ZhiJiaoHubQuery(normalizedSource, ForceRefresh: true, MirrorSource: normalizedMirror);
|
||||
var result = await GetZhiJiaoHubImagesAsync(query, cancellationToken);
|
||||
|
||||
if (!result.Success || result.Data == null)
|
||||
{
|
||||
return RecommendationQueryResult<ZhiJiaoHubHybridSnapshot>.Fail(
|
||||
result.ErrorCode ?? "upstream_error",
|
||||
result.ErrorMessage ?? "Failed to fetch image list");
|
||||
}
|
||||
|
||||
var hybridImages = result.Data.Images.Select((img, idx) =>
|
||||
{
|
||||
var hasLocal = localPathMap.TryGetValue(img.Url, out var localPath);
|
||||
return new ZhiJiaoHubHybridImageItem(
|
||||
img.Name,
|
||||
img.Url,
|
||||
hasLocal ? localPath : null,
|
||||
idx,
|
||||
hasLocal);
|
||||
}).ToList();
|
||||
|
||||
var snapshot = new ZhiJiaoHubHybridSnapshot(
|
||||
hybridImages,
|
||||
normalizedSource,
|
||||
hybridImages.Count(i => i.IsCached),
|
||||
hybridImages.Count);
|
||||
|
||||
return RecommendationQueryResult<ZhiJiaoHubHybridSnapshot>.Ok(snapshot);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return RecommendationQueryResult<ZhiJiaoHubHybridSnapshot>.Fail("upstream_network_error", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> DownloadAndCacheImageAsync(
|
||||
string source,
|
||||
ZhiJiaoHubImageItem image,
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
|
||||
var normalizedMirror = ZhiJiaoHubMirrorSources.Normalize(mirrorSource);
|
||||
|
||||
return await _zhiJiaoHubCacheService.DownloadAndSaveImageAsync(
|
||||
normalizedSource,
|
||||
image.Name,
|
||||
image.Url,
|
||||
normalizedMirror,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public Task StartBackgroundDownloadAsync(
|
||||
string source,
|
||||
IReadOnlyList<ZhiJiaoHubHybridImageItem> images,
|
||||
string mirrorSource,
|
||||
Action<int, int, string>? onProgress = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
|
||||
var normalizedMirror = ZhiJiaoHubMirrorSources.Normalize(mirrorSource);
|
||||
|
||||
return Task.Run(async () =>
|
||||
{
|
||||
var uncachedImages = images.Where(i => !i.IsCached).ToList();
|
||||
var total = uncachedImages.Count;
|
||||
var downloaded = 0;
|
||||
|
||||
foreach (var image in uncachedImages)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var localPath = await _zhiJiaoHubCacheService.DownloadAndSaveImageAsync(
|
||||
normalizedSource,
|
||||
image.Name,
|
||||
image.RemoteUrl,
|
||||
normalizedMirror,
|
||||
cancellationToken);
|
||||
|
||||
if (localPath != null)
|
||||
{
|
||||
downloaded++;
|
||||
}
|
||||
|
||||
onProgress?.Invoke(downloaded, total, image.Name);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}, cancellationToken);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Security.Authentication;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using LanMountainDesktop.Models;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
@@ -139,6 +140,119 @@ public sealed class ZhiJiaoHubCacheService : IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, string> LoadLocalPathMap(string source)
|
||||
{
|
||||
lock (_manifestLock)
|
||||
{
|
||||
var result = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
if (!File.Exists(_manifestPath))
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<CacheManifest>(json, JsonOptions);
|
||||
if (manifest?.Entries?.TryGetValue(source, out var entry) != true)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
|
||||
var sourceDir = GetSourceDirectory(source);
|
||||
foreach (var img in entry.Images)
|
||||
{
|
||||
var localPath = Path.Combine(sourceDir, img.LocalFileName);
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
result[img.OriginalUrl] = localPath;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public string? GetLocalPath(string source, string originalUrl)
|
||||
{
|
||||
lock (_manifestLock)
|
||||
{
|
||||
if (!File.Exists(_manifestPath))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_manifestPath);
|
||||
var manifest = JsonSerializer.Deserialize<CacheManifest>(json, JsonOptions);
|
||||
if (manifest?.Entries?.TryGetValue(source, out var entry) != true)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var img = entry.Images.FirstOrDefault(i =>
|
||||
string.Equals(i.OriginalUrl, originalUrl, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (img == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var sourceDir = GetSourceDirectory(source);
|
||||
var localPath = Path.Combine(sourceDir, img.LocalFileName);
|
||||
return File.Exists(localPath) ? localPath : null;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string?> DownloadAndSaveImageAsync(
|
||||
string source,
|
||||
string name,
|
||||
string remoteUrl,
|
||||
string mirrorSource,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
var sourceDir = GetSourceDirectory(source);
|
||||
Directory.CreateDirectory(sourceDir);
|
||||
|
||||
var fileName = GetSafeFileName(name, remoteUrl);
|
||||
var localPath = Path.Combine(sourceDir, fileName);
|
||||
|
||||
if (File.Exists(localPath))
|
||||
{
|
||||
AddToManifest(source, name, remoteUrl, fileName);
|
||||
return localPath;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var downloadUrl = ResolveDownloadUrl(remoteUrl, mirrorSource);
|
||||
using var response = await DownloadClient.GetAsync(downloadUrl, cancellationToken);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
await using var fileStream = File.Create(localPath);
|
||||
await response.Content.CopyToAsync(fileStream, cancellationToken);
|
||||
|
||||
AddToManifest(source, name, remoteUrl, fileName);
|
||||
return localPath;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<ZhiJiaoHubSyncResult> SyncImagesAsync(
|
||||
string source,
|
||||
IReadOnlyList<ZhiJiaoHubImageItem> remoteImages,
|
||||
@@ -159,12 +273,6 @@ public sealed class ZhiJiaoHubCacheService : IDisposable
|
||||
var failedCount = 0;
|
||||
var localImages = new List<CachedImageInfo>();
|
||||
|
||||
var existingFiles = new HashSet<string>(
|
||||
Directory.Exists(sourceDir)
|
||||
? Directory.GetFiles(sourceDir, "*.jpg").Concat(Directory.GetFiles(sourceDir, "*.png")).Concat(Directory.GetFiles(sourceDir, "*.gif"))
|
||||
: Array.Empty<string>(),
|
||||
StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
for (var i = 0; i < remoteImages.Count; i++)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
@@ -291,6 +399,51 @@ public sealed class ZhiJiaoHubCacheService : IDisposable
|
||||
return originalUrl;
|
||||
}
|
||||
|
||||
private void AddToManifest(string source, string name, string originalUrl, string localFileName)
|
||||
{
|
||||
lock (_manifestLock)
|
||||
{
|
||||
CacheManifest manifest;
|
||||
if (File.Exists(_manifestPath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = File.ReadAllText(_manifestPath);
|
||||
manifest = JsonSerializer.Deserialize<CacheManifest>(json, JsonOptions) ?? new CacheManifest();
|
||||
}
|
||||
catch
|
||||
{
|
||||
manifest = new CacheManifest();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
manifest = new CacheManifest();
|
||||
}
|
||||
|
||||
if (!manifest.Entries.TryGetValue(source, out var entry))
|
||||
{
|
||||
entry = new CacheEntry(new List<CachedImageInfo>(), DateTimeOffset.UtcNow);
|
||||
manifest.Entries[source] = entry;
|
||||
}
|
||||
|
||||
var existingIndex = entry.Images.FindIndex(i =>
|
||||
string.Equals(i.OriginalUrl, originalUrl, StringComparison.OrdinalIgnoreCase));
|
||||
|
||||
if (existingIndex >= 0)
|
||||
{
|
||||
entry.Images[existingIndex] = new CachedImageInfo(name, originalUrl, localFileName);
|
||||
}
|
||||
else
|
||||
{
|
||||
entry.Images.Add(new CachedImageInfo(name, originalUrl, localFileName));
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
|
||||
File.WriteAllText(_manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
|
||||
}
|
||||
}
|
||||
|
||||
private void SaveManifest(string source, List<CachedImageInfo> images)
|
||||
{
|
||||
lock (_manifestLock)
|
||||
|
||||
Reference in New Issue
Block a user