This commit is contained in:
lincube
2026-03-29 15:34:17 +08:00
parent 372b5b7adc
commit bd2313fe7e
23 changed files with 2986 additions and 22 deletions

View File

@@ -262,7 +262,12 @@ public static class DesktopComponentEditorRegistryFactory
nameof(ComponentSettingsSnapshot.Stcn24ForumAutoRefreshIntervalMinutes),
nameof(ComponentSettingsSnapshot.Stcn24ForumSourceType)
]
}))
})),
[BuiltInComponentIds.DesktopZhiJiaoHub] = new(
BuiltInComponentIds.DesktopZhiJiaoHub,
context => new ZhiJiaoHubComponentEditor(context),
preferredWidth: 480d,
preferredHeight: 520d)
};
foreach (var componentId in GetBuiltInDesktopComponentIds(componentRegistry))

View File

@@ -107,7 +107,8 @@ public sealed class GitHubReleaseUpdateService : IDisposable
bool includePrerelease,
CancellationToken cancellationToken = default)
{
var normalizedCurrentVersionText = NormalizeVersion(currentVersion).ToString(3);
var normalizedCurrentVersion = NormalizeVersion(currentVersion);
var normalizedCurrentVersionText = FormatVersionText(normalizedCurrentVersion);
if (string.IsNullOrWhiteSpace(_owner) || string.IsNullOrWhiteSpace(_repo))
{
@@ -141,7 +142,7 @@ public sealed class GitHubReleaseUpdateService : IDisposable
var hasParsedTagVersion = TryParseVersion(release.TagName, out var parsedTagVersion);
var latestVersionText = hasParsedTagVersion && parsedTagVersion is not null
? parsedTagVersion.ToString(3)
? FormatVersionText(parsedTagVersion)
: release.TagName;
var isUpdateAvailable = parsedTagVersion is not null && parsedTagVersion > currentVersion;
@@ -180,7 +181,8 @@ public sealed class GitHubReleaseUpdateService : IDisposable
bool includePrerelease,
CancellationToken cancellationToken = default)
{
var normalizedCurrentVersionText = NormalizeVersion(currentVersion).ToString(3);
var normalizedCurrentVersion = NormalizeVersion(currentVersion);
var normalizedCurrentVersionText = FormatVersionText(normalizedCurrentVersion);
if (string.IsNullOrWhiteSpace(_owner) || string.IsNullOrWhiteSpace(_repo))
{
@@ -216,7 +218,7 @@ public sealed class GitHubReleaseUpdateService : IDisposable
var hasParsedTagVersion = TryParseVersion(release.TagName, out var parsedTagVersion);
var latestVersionText = hasParsedTagVersion && parsedTagVersion is not null
? parsedTagVersion.ToString(3)
? FormatVersionText(parsedTagVersion)
: release.TagName;
var preferredAsset = SelectPreferredInstallerAsset(release.Assets);
@@ -740,8 +742,18 @@ public sealed class GitHubReleaseUpdateService : IDisposable
{
var major = Math.Max(0, version.Major);
var minor = Math.Max(0, version.Minor);
var build = Math.Max(0, version.Build);
return new Version(major, minor, build);
var build = Math.Max(0, version.Build >= 0 ? version.Build : 0);
var revision = Math.Max(0, version.Revision >= 0 ? version.Revision : 0);
return revision > 0
? new Version(major, minor, build, revision)
: new Version(major, minor, build);
}
private static string FormatVersionText(Version version)
{
return version.Revision > 0
? version.ToString(4)
: version.ToString(3);
}
private static string Truncate(string value, int maxLength)

View File

@@ -52,6 +52,22 @@ public sealed record ExchangeRateQuery(
string? TargetCurrency = null,
bool ForceRefresh = false);
public sealed record ZhiJiaoHubQuery(
string? Source = null,
int? ImageIndex = null,
bool ForceRefresh = false,
string? MirrorSource = null);
public sealed record ZhiJiaoHubImageItem(
string Name,
string Url,
int Index);
public sealed record ZhiJiaoHubSnapshot(
IReadOnlyList<ZhiJiaoHubImageItem> Images,
int CurrentIndex,
string Source);
public sealed record RecommendationQueryResult<T>(
bool Success,
T? Data,
@@ -285,6 +301,14 @@ public sealed record RecommendationApiOptions
public int DefaultBaiduHotSearchCount { get; init; } = 4;
public int DefaultStcn24ForumPostCount { get; init; } = 4;
public string ClassIslandHubApiUrl { get; init; } = "https://api.github.com/repos/ClassIsland/classisland-hub/contents/images";
public string SectlHubApiUrl { get; init; } = "https://api.github.com/repos/SECTL/SECTL-hub/contents/images";
public string ClassIslandHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/ClassIsland/classisland-hub/main/images/{0}";
public string SectlHubRawUrlTemplate { get; init; } = "https://raw.githubusercontent.com/SECTL/SECTL-hub/main/images/{0}";
}
public interface IRecommendationInfoService
@@ -325,5 +349,19 @@ public interface IRecommendationInfoService
ExchangeRateQuery query,
CancellationToken cancellationToken = default);
Task<RecommendationQueryResult<ZhiJiaoHubSnapshot>> GetZhiJiaoHubImagesAsync(
ZhiJiaoHubQuery query,
CancellationToken cancellationToken = default);
Task<ZhiJiaoHubSyncResult> SyncZhiJiaoHubImagesAsync(
string source,
string mirrorSource,
IProgress<(int Current, int Total, string Status)>? progress = null,
CancellationToken cancellationToken = default);
ZhiJiaoHubLocalSnapshot? LoadZhiJiaoHubLocalSnapshot(string source);
bool HasZhiJiaoHubLocalCache(string source);
void ClearCache();
}

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
@@ -53,6 +53,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
Dictionary<string, decimal> Rates,
DateTimeOffset ExpireAt,
DateTimeOffset FetchedAt);
private sealed record ZhiJiaoHubCacheEntry(ZhiJiaoHubSnapshot Snapshot, DateTimeOffset ExpireAt);
private sealed record ArtworkCandidate(
string Title,
string? Artist,
@@ -80,6 +81,8 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ExchangeRateTableCacheEntry> _exchangeRateCacheByBaseCurrency =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ZhiJiaoHubCacheEntry> _zhiJiaoHubCacheBySource =
new(StringComparer.OrdinalIgnoreCase);
private int _dailyNewsRotationCursor;
static RecommendationDataService()
@@ -94,7 +97,15 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
_options = options ?? new RecommendationApiOptions();
if (httpClient is null)
{
_httpClient = new HttpClient
// 配置 HttpClientHandler 以支持所有 TLS 版本
var handler = new HttpClientHandler
{
SslProtocols = System.Security.Authentication.SslProtocols.Tls12 |
System.Security.Authentication.SslProtocols.Tls13,
ServerCertificateCustomValidationCallback = (message, cert, chain, errors) => true
};
_httpClient = new HttpClient(handler)
{
Timeout = _options.RequestTimeout
};
@@ -128,6 +139,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
_dailyWordCache = null;
_stcn24ForumPostsCacheBySource.Clear();
_exchangeRateCacheByBaseCurrency.Clear();
_zhiJiaoHubCacheBySource.Clear();
}
}
@@ -3194,4 +3206,254 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
? text
: $"{text[..maxLength]}...";
}
// 智教Hub相关方法
public async Task<RecommendationQueryResult<ZhiJiaoHubSnapshot>> GetZhiJiaoHubImagesAsync(
ZhiJiaoHubQuery query,
CancellationToken cancellationToken = default)
{
var normalizedQuery = query ?? new ZhiJiaoHubQuery();
var source = ZhiJiaoHubSources.Normalize(normalizedQuery.Source);
var mirrorSource = ZhiJiaoHubMirrorSources.Normalize(normalizedQuery.MirrorSource);
var cacheKey = $"{source}|{mirrorSource}";
if (!normalizedQuery.ForceRefresh && TryGetZhiJiaoHubFromCache(cacheKey, out var cached))
{
return RecommendationQueryResult<ZhiJiaoHubSnapshot>.Ok(cached);
}
try
{
var snapshot = await FetchZhiJiaoHubSnapshotAsync(source, mirrorSource, cancellationToken);
SetZhiJiaoHubCache(cacheKey, snapshot);
return RecommendationQueryResult<ZhiJiaoHubSnapshot>.Ok(snapshot);
}
catch (OperationCanceledException)
{
throw;
}
catch (HttpRequestException ex)
{
return RecommendationQueryResult<ZhiJiaoHubSnapshot>.Fail("upstream_network_error", ex.Message);
}
catch (Exception ex)
{
return RecommendationQueryResult<ZhiJiaoHubSnapshot>.Fail("upstream_parse_error", ex.Message);
}
}
private async Task<ZhiJiaoHubSnapshot> FetchZhiJiaoHubSnapshotAsync(string source, string mirrorSource, CancellationToken cancellationToken)
{
var (owner, repo, path) = source switch
{
ZhiJiaoHubSources.Sectl => ("SECTL", "SECTL-hub", "docs/.vuepress/public/images"),
_ => ("ClassIsland", "classisland-hub", "images")
};
var contentsUrl = $"https://api.github.com/repos/{owner}/{repo}/contents/{path}";
// 如果使用镜像加速,代理 GitHub API 请求
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
{
contentsUrl = ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + contentsUrl;
}
try
{
var images = await FetchImagesFromContentsApi(owner, repo, path, contentsUrl, mirrorSource, cancellationToken);
if (images.Count == 0)
{
throw new InvalidOperationException("未找到图片文件");
}
// 随机打乱图片顺序
var random = new Random();
var shuffled = images.OrderBy(_ => random.Next()).ToList();
// 重新设置索引
for (int i = 0; i < shuffled.Count; i++)
{
var item = shuffled[i];
shuffled[i] = item with { Index = i };
}
return new ZhiJiaoHubSnapshot(shuffled, 0, source);
}
catch (HttpRequestException ex) when (ex.Message.Contains("403") || ex.Message.Contains("rate limit"))
{
throw new HttpRequestException("GitHub API 速率限制,请稍后重试");
}
catch (Exception ex)
{
throw new HttpRequestException($"获取图片列表失败: {ex.Message}");
}
}
private async Task<List<ZhiJiaoHubImageItem>> FetchImagesFromContentsApi(string owner, string repo, string path, string contentsUrl, string mirrorSource, CancellationToken cancellationToken)
{
var images = new List<ZhiJiaoHubImageItem>();
using var request = new HttpRequestMessage(HttpMethod.Get, contentsUrl);
request.Headers.TryAddWithoutValidation("User-Agent", "LanMountainDesktop/1.0");
request.Headers.TryAddWithoutValidation("Accept", "application/vnd.github+json");
request.Headers.TryAddWithoutValidation("X-GitHub-Api-Version", "2022-11-28");
using var response = await _httpClient.SendAsync(request, cancellationToken);
if (!response.IsSuccessStatusCode)
{
var errorText = await response.Content.ReadAsStringAsync(cancellationToken);
if ((int)response.StatusCode == 403)
{
throw new HttpRequestException("GitHub API 速率限制,请稍后重试");
}
throw new HttpRequestException($"API 返回错误: {(int)response.StatusCode} - {Truncate(errorText, 200)}");
}
var responseText = await response.Content.ReadAsStringAsync(cancellationToken);
using var document = JsonDocument.Parse(responseText);
var root = document.RootElement;
if (root.ValueKind != JsonValueKind.Array)
{
if (root.ValueKind == JsonValueKind.Object && root.TryGetProperty("message", out var messageNode))
{
var errorMessage = messageNode.GetString();
throw new InvalidOperationException($"GitHub API 错误: {errorMessage}");
}
throw new InvalidOperationException("Invalid response format from GitHub API.");
}
int index = 0;
foreach (var item in root.EnumerateArray())
{
var type = ReadString(item, "type");
if (type != "file")
{
continue;
}
var name = ReadString(item, "name");
var downloadUrl = ReadString(item, "download_url");
if (string.IsNullOrWhiteSpace(name))
{
continue;
}
// 只处理图片文件
var extension = Path.GetExtension(name).ToLowerInvariant();
if (extension != ".png" && extension != ".jpg" && extension != ".jpeg" && extension != ".gif" && extension != ".webp")
{
continue;
}
// 解码文件名
var decodedName = Uri.UnescapeDataString(name);
decodedName = Path.GetFileNameWithoutExtension(decodedName);
// 构造图片 URL
string imageUrl;
if (!string.IsNullOrWhiteSpace(downloadUrl))
{
imageUrl = downloadUrl;
}
else
{
imageUrl = $"https://raw.githubusercontent.com/{owner}/{repo}/main/{path}/{Uri.EscapeDataString(name)}";
}
// 应用镜像加速到图片 URL
imageUrl = ZhiJiaoHubMirrorSources.ApplyMirror(imageUrl, mirrorSource);
images.Add(new ZhiJiaoHubImageItem(decodedName, imageUrl, index));
index++;
}
return images;
}
private bool TryGetZhiJiaoHubFromCache(string cacheKey, out ZhiJiaoHubSnapshot snapshot)
{
lock (_cacheGate)
{
if (_zhiJiaoHubCacheBySource.TryGetValue(cacheKey, out var cacheEntry) &&
cacheEntry.ExpireAt > DateTimeOffset.UtcNow)
{
snapshot = cacheEntry.Snapshot;
return true;
}
}
snapshot = null!;
return false;
}
private void SetZhiJiaoHubCache(string cacheKey, ZhiJiaoHubSnapshot snapshot)
{
lock (_cacheGate)
{
// 使用较长的缓存时间1小时因为图片列表不常变化
_zhiJiaoHubCacheBySource[cacheKey] = new ZhiJiaoHubCacheEntry(
snapshot,
DateTimeOffset.UtcNow.Add(TimeSpan.FromHours(1)));
}
}
private readonly ZhiJiaoHubCacheService _zhiJiaoHubCacheService = new();
public async Task<ZhiJiaoHubSyncResult> SyncZhiJiaoHubImagesAsync(
string source,
string mirrorSource,
IProgress<(int Current, int Total, string Status)>? progress = null,
CancellationToken cancellationToken = default)
{
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
var normalizedMirror = ZhiJiaoHubMirrorSources.Normalize(mirrorSource);
try
{
var query = new ZhiJiaoHubQuery(normalizedSource, ForceRefresh: true, MirrorSource: normalizedMirror);
var result = await GetZhiJiaoHubImagesAsync(query, cancellationToken);
if (!result.Success || result.Data == null)
{
return new ZhiJiaoHubSyncResult(
false,
null,
0,
0,
0,
result.ErrorMessage ?? "Failed to fetch image list");
}
return await _zhiJiaoHubCacheService.SyncImagesAsync(
normalizedSource,
result.Data.Images,
normalizedMirror,
progress,
cancellationToken);
}
catch (OperationCanceledException)
{
throw;
}
catch (Exception ex)
{
return new ZhiJiaoHubSyncResult(false, null, 0, 0, 0, ex.Message);
}
}
public ZhiJiaoHubLocalSnapshot? LoadZhiJiaoHubLocalSnapshot(string source)
{
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
return _zhiJiaoHubCacheService.LoadLocalSnapshot(normalizedSource);
}
public bool HasZhiJiaoHubLocalCache(string source)
{
var normalizedSource = ZhiJiaoHubSources.Normalize(source);
return _zhiJiaoHubCacheService.HasLocalCache(normalizedSource);
}
}

View File

@@ -0,0 +1,359 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Authentication;
using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
namespace LanMountainDesktop.Services;
public sealed record ZhiJiaoHubLocalImageItem(
string Name,
string OriginalUrl,
string LocalPath,
int Index);
public sealed record ZhiJiaoHubLocalSnapshot(
IReadOnlyList<ZhiJiaoHubLocalImageItem> Images,
string Source,
DateTimeOffset LastUpdated,
int TotalCount);
public sealed record ZhiJiaoHubSyncResult(
bool Success,
ZhiJiaoHubLocalSnapshot? Snapshot,
int DownloadedCount,
int SkippedCount,
int FailedCount,
string? ErrorMessage = null);
public sealed class ZhiJiaoHubCacheService : IDisposable
{
private static readonly HttpClient DownloadClient;
private static readonly JsonSerializerOptions JsonOptions = new()
{
PropertyNameCaseInsensitive = true,
WriteIndented = true
};
private readonly string _cacheDirectory;
private readonly string _manifestPath;
private readonly object _manifestLock = new();
private bool _isDisposed;
static ZhiJiaoHubCacheService()
{
var handler = new HttpClientHandler
{
SslProtocols = SslProtocols.Tls12 | SslProtocols.Tls13,
AutomaticDecompression = System.Net.DecompressionMethods.GZip | System.Net.DecompressionMethods.Deflate
};
DownloadClient = new HttpClient(handler)
{
Timeout = TimeSpan.FromSeconds(30)
};
DownloadClient.DefaultRequestHeaders.UserAgent.ParseAdd("LanMountainDesktop/1.0");
}
public ZhiJiaoHubCacheService()
{
var appData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
var dataDirectory = Path.Combine(appData, "LanMountainDesktop", "cache", "zhijiaohub");
_cacheDirectory = dataDirectory;
_manifestPath = Path.Combine(dataDirectory, "manifest.json");
}
public string CacheDirectory => _cacheDirectory;
public bool HasLocalCache(string source)
{
lock (_manifestLock)
{
if (!File.Exists(_manifestPath))
{
return false;
}
try
{
var json = File.ReadAllText(_manifestPath);
var manifest = JsonSerializer.Deserialize<CacheManifest>(json, JsonOptions);
return manifest?.Entries?.ContainsKey(source) == true &&
manifest.Entries[source].Images.Count > 0 &&
Directory.Exists(GetSourceDirectory(source));
}
catch
{
return false;
}
}
}
public ZhiJiaoHubLocalSnapshot? LoadLocalSnapshot(string source)
{
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 sourceDir = GetSourceDirectory(source);
var images = entry.Images
.Where(img => File.Exists(Path.Combine(sourceDir, img.LocalFileName)))
.Select((img, idx) => new ZhiJiaoHubLocalImageItem(
img.Name,
img.OriginalUrl,
Path.Combine(sourceDir, img.LocalFileName),
idx))
.ToList();
if (images.Count == 0)
{
return null;
}
return new ZhiJiaoHubLocalSnapshot(
images,
source,
entry.LastUpdated,
images.Count);
}
catch
{
return null;
}
}
}
public async Task<ZhiJiaoHubSyncResult> SyncImagesAsync(
string source,
IReadOnlyList<ZhiJiaoHubImageItem> remoteImages,
string mirrorSource,
IProgress<(int Current, int Total, string Status)>? progress = null,
CancellationToken cancellationToken = default)
{
if (remoteImages == null || remoteImages.Count == 0)
{
return new ZhiJiaoHubSyncResult(false, null, 0, 0, 0, "No images to sync");
}
var sourceDir = GetSourceDirectory(source);
Directory.CreateDirectory(sourceDir);
var downloadedCount = 0;
var skippedCount = 0;
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();
var remoteImage = remoteImages[i];
var fileName = GetSafeFileName(remoteImage.Name, remoteImage.Url);
var localPath = Path.Combine(sourceDir, fileName);
progress?.Report((i + 1, remoteImages.Count, $"Downloading {remoteImage.Name}..."));
if (File.Exists(localPath))
{
skippedCount++;
localImages.Add(new CachedImageInfo(remoteImage.Name, remoteImage.Url, fileName));
continue;
}
try
{
var downloadUrl = ResolveDownloadUrl(remoteImage.Url, mirrorSource);
using var response = await DownloadClient.GetAsync(downloadUrl, cancellationToken);
response.EnsureSuccessStatusCode();
await using var fileStream = File.Create(localPath);
await response.Content.CopyToAsync(fileStream, cancellationToken);
downloadedCount++;
localImages.Add(new CachedImageInfo(remoteImage.Name, remoteImage.Url, fileName));
}
catch (Exception)
{
failedCount++;
}
}
if (localImages.Count == 0)
{
return new ZhiJiaoHubSyncResult(false, null, downloadedCount, skippedCount, failedCount, "All downloads failed");
}
SaveManifest(source, localImages);
var snapshot = new ZhiJiaoHubLocalSnapshot(
localImages.Select((img, idx) => new ZhiJiaoHubLocalImageItem(
img.Name,
img.OriginalUrl,
Path.Combine(sourceDir, img.LocalFileName),
idx)).ToList(),
source,
DateTimeOffset.UtcNow,
localImages.Count);
return new ZhiJiaoHubSyncResult(true, snapshot, downloadedCount, skippedCount, failedCount);
}
public void ClearCache(string? source = null)
{
lock (_manifestLock)
{
if (source != null)
{
var sourceDir = GetSourceDirectory(source);
if (Directory.Exists(sourceDir))
{
Directory.Delete(sourceDir, true);
}
if (File.Exists(_manifestPath))
{
try
{
var json = File.ReadAllText(_manifestPath);
var manifest = JsonSerializer.Deserialize<CacheManifest>(json, JsonOptions);
if (manifest?.Entries != null && manifest.Entries.ContainsKey(source))
{
manifest.Entries.Remove(source);
File.WriteAllText(_manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
}
}
catch
{
}
}
}
else
{
if (Directory.Exists(_cacheDirectory))
{
Directory.Delete(_cacheDirectory, true);
}
}
}
}
private string GetSourceDirectory(string source)
{
return Path.Combine(_cacheDirectory, source.ToLowerInvariant().Replace(" ", "-"));
}
private static string GetSafeFileName(string name, string url)
{
var ext = Path.GetExtension(new Uri(url).AbsolutePath);
if (string.IsNullOrEmpty(ext) || ext.Length > 5)
{
ext = ".jpg";
}
var safeName = string.Concat(name.Split(Path.GetInvalidFileNameChars()));
if (string.IsNullOrWhiteSpace(safeName))
{
safeName = Guid.NewGuid().ToString("N")[..8];
}
return $"{safeName}{ext}";
}
private static string ResolveDownloadUrl(string originalUrl, string mirrorSource)
{
if (string.Equals(mirrorSource, ZhiJiaoHubMirrorSources.GhProxy, StringComparison.OrdinalIgnoreCase))
{
return ZhiJiaoHubMirrorSources.GhProxyBaseUrl.TrimEnd('/') + "/" + originalUrl;
}
return originalUrl;
}
private void SaveManifest(string source, List<CachedImageInfo> images)
{
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();
}
manifest.Entries[source] = new CacheEntry(images, DateTimeOffset.UtcNow);
Directory.CreateDirectory(Path.GetDirectoryName(_manifestPath)!);
File.WriteAllText(_manifestPath, JsonSerializer.Serialize(manifest, JsonOptions));
}
}
public void Dispose()
{
if (_isDisposed) return;
_isDisposed = true;
}
private sealed class CacheManifest
{
public Dictionary<string, CacheEntry> Entries { get; set; } = new();
}
private sealed class CacheEntry
{
public List<CachedImageInfo> Images { get; set; }
public DateTimeOffset LastUpdated { get; set; }
public CacheEntry(List<CachedImageInfo> images, DateTimeOffset lastUpdated)
{
Images = images;
LastUpdated = lastUpdated;
}
}
private sealed class CachedImageInfo
{
public string Name { get; set; }
public string OriginalUrl { get; set; }
public string LocalFileName { get; set; }
public CachedImageInfo(string name, string originalUrl, string localFileName)
{
Name = name;
OriginalUrl = originalUrl;
LocalFileName = localFileName;
}
}
}