引入了托盘菜单,提供了应用启动台隐藏应用功能,优化了自动刷新功能,为STCN 24组件提供了更多信息选项。
This commit is contained in:
lincube
2026-03-06 10:32:02 +08:00
parent de40471af6
commit 72a0be16b3
29 changed files with 1752 additions and 153 deletions

View File

@@ -16,9 +16,6 @@ public sealed class ComponentSettingsService
private static readonly object CacheGate = new();
private static readonly TimeSpan CacheProbeInterval = TimeSpan.FromMilliseconds(400);
private static readonly int[] SupportedCnrIntervals = [5, 10, 40, 60, 720, 1440];
private static readonly int[] SupportedDailyWordIntervals = [30, 60, 180, 360, 720, 1440];
private static readonly int[] SupportedBilibiliHotSearchIntervals = [5, 10, 15, 30, 60, 180];
private static string? _cachedPath;
private static ComponentSettingsSnapshot? _cachedSnapshot;
@@ -185,7 +182,12 @@ public sealed class ComponentSettingsService
DailyWordAutoRefreshEnabled = legacy.DailyWordAutoRefreshEnabled,
DailyWordAutoRefreshIntervalMinutes = legacy.DailyWordAutoRefreshIntervalMinutes,
BilibiliHotSearchAutoRefreshEnabled = legacy.BilibiliHotSearchAutoRefreshEnabled,
BilibiliHotSearchAutoRefreshIntervalMinutes = legacy.BilibiliHotSearchAutoRefreshIntervalMinutes
BilibiliHotSearchAutoRefreshIntervalMinutes = legacy.BilibiliHotSearchAutoRefreshIntervalMinutes,
WeatherAutoRefreshEnabled = legacy.WeatherAutoRefreshEnabled,
WeatherAutoRefreshIntervalMinutes = legacy.WeatherAutoRefreshIntervalMinutes,
Stcn24ForumAutoRefreshEnabled = legacy.Stcn24ForumAutoRefreshEnabled,
Stcn24ForumAutoRefreshIntervalMinutes = legacy.Stcn24ForumAutoRefreshIntervalMinutes,
Stcn24ForumSourceType = legacy.Stcn24ForumSourceType
};
return true;
@@ -237,6 +239,9 @@ public sealed class ComponentSettingsService
normalized.DailyWordAutoRefreshIntervalMinutes = NormalizeDailyWordInterval(normalized.DailyWordAutoRefreshIntervalMinutes);
normalized.BilibiliHotSearchAutoRefreshIntervalMinutes = NormalizeBilibiliHotSearchInterval(
normalized.BilibiliHotSearchAutoRefreshIntervalMinutes);
normalized.WeatherAutoRefreshIntervalMinutes = NormalizeWeatherInterval(normalized.WeatherAutoRefreshIntervalMinutes);
normalized.Stcn24ForumAutoRefreshIntervalMinutes = NormalizeStcn24ForumInterval(normalized.Stcn24ForumAutoRefreshIntervalMinutes);
normalized.Stcn24ForumSourceType = Stcn24ForumSourceTypes.Normalize(normalized.Stcn24ForumSourceType);
return normalized;
}
@@ -311,53 +316,27 @@ public sealed class ComponentSettingsService
private static int NormalizeCnrInterval(int minutes)
{
if (minutes <= 0)
{
return 60;
}
if (SupportedCnrIntervals.Contains(minutes))
{
return minutes;
}
return SupportedCnrIntervals
.OrderBy(value => Math.Abs(value - minutes))
.FirstOrDefault(60);
return RefreshIntervalCatalog.Normalize(minutes, 60);
}
private static int NormalizeDailyWordInterval(int minutes)
{
if (minutes <= 0)
{
return 360;
}
if (SupportedDailyWordIntervals.Contains(minutes))
{
return minutes;
}
return SupportedDailyWordIntervals
.OrderBy(value => Math.Abs(value - minutes))
.FirstOrDefault(360);
return RefreshIntervalCatalog.Normalize(minutes, 360);
}
private static int NormalizeBilibiliHotSearchInterval(int minutes)
{
if (minutes <= 0)
{
return 15;
}
return RefreshIntervalCatalog.Normalize(minutes, 15);
}
if (SupportedBilibiliHotSearchIntervals.Contains(minutes))
{
return minutes;
}
private static int NormalizeWeatherInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 12);
}
return SupportedBilibiliHotSearchIntervals
.OrderBy(value => Math.Abs(value - minutes))
.FirstOrDefault(15);
private static int NormalizeStcn24ForumInterval(int minutes)
{
return RefreshIntervalCatalog.Normalize(minutes, 20);
}
private void UpdateCache(ComponentSettingsSnapshot snapshot, DateTime writeTimeUtc, DateTime probeTimeUtc)
@@ -399,5 +378,15 @@ public sealed class ComponentSettingsService
public bool BilibiliHotSearchAutoRefreshEnabled { get; set; } = true;
public int BilibiliHotSearchAutoRefreshIntervalMinutes { get; set; } = 15;
public bool WeatherAutoRefreshEnabled { get; set; } = true;
public int WeatherAutoRefreshIntervalMinutes { get; set; } = 12;
public bool Stcn24ForumAutoRefreshEnabled { get; set; } = true;
public int Stcn24ForumAutoRefreshIntervalMinutes { get; set; } = 20;
public string Stcn24ForumSourceType { get; set; } = Stcn24ForumSourceTypes.LatestCreated;
}
}

View File

@@ -32,6 +32,7 @@ public sealed record DailyWordQuery(
public sealed record Stcn24ForumPostsQuery(
string? Locale = null,
int? ItemCount = null,
string? SourceType = null,
bool ForceRefresh = false);
public sealed record ExchangeRateQuery(

View File

@@ -61,7 +61,8 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
private DailyNewsCacheEntry? _dailyNewsCache;
private BilibiliHotSearchCacheEntry? _bilibiliHotSearchCache;
private DailyWordCacheEntry? _dailyWordCache;
private Stcn24ForumPostsCacheEntry? _stcn24ForumPostsCache;
private readonly Dictionary<string, Stcn24ForumPostsCacheEntry> _stcn24ForumPostsCacheBySource =
new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, ExchangeRateTableCacheEntry> _exchangeRateCacheByBaseCurrency =
new(StringComparer.OrdinalIgnoreCase);
private int _dailyNewsRotationCursor;
@@ -108,7 +109,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
_dailyNewsCache = null;
_bilibiliHotSearchCache = null;
_dailyWordCache = null;
_stcn24ForumPostsCache = null;
_stcn24ForumPostsCacheBySource.Clear();
_exchangeRateCacheByBaseCurrency.Clear();
}
}
@@ -348,12 +349,13 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
CancellationToken cancellationToken = default)
{
var normalizedQuery = query ?? new Stcn24ForumPostsQuery();
var sourceType = Stcn24ForumSourceTypes.Normalize(normalizedQuery.SourceType);
var targetCount = normalizedQuery.ItemCount.HasValue
? Math.Clamp(normalizedQuery.ItemCount.Value, 1, 12)
: Math.Clamp(_options.DefaultStcn24ForumPostCount, 1, 12);
if (!normalizedQuery.ForceRefresh &&
TryGetStcn24ForumPostsFromCache(out var cached) &&
TryGetStcn24ForumPostsFromCache(sourceType, out var cached) &&
cached.Items.Count >= targetCount)
{
var projectedSnapshot = cached with
@@ -365,7 +367,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
try
{
var snapshot = await FetchStcn24ForumPostsSnapshotAsync(targetCount, cancellationToken);
var snapshot = await FetchStcn24ForumPostsSnapshotAsync(targetCount, sourceType, cancellationToken);
if (snapshot.Items.Count == 0)
{
return RecommendationQueryResult<Stcn24ForumPostsSnapshot>.Fail(
@@ -373,7 +375,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
"No STCN forum posts were returned.");
}
SetStcn24ForumPostsCache(snapshot);
SetStcn24ForumPostsCache(sourceType, snapshot);
return RecommendationQueryResult<Stcn24ForumPostsSnapshot>.Ok(snapshot);
}
catch (OperationCanceledException)
@@ -814,6 +816,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
private async Task<Stcn24ForumPostsSnapshot> FetchStcn24ForumPostsSnapshotAsync(
int targetCount,
string sourceType,
CancellationToken cancellationToken)
{
var safeCount = Math.Clamp(targetCount, 1, 12);
@@ -824,11 +827,21 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
keyword = "STCN";
}
var sortToken = ResolveSmartTeachDiscussionSortToken(sourceType);
var requestUrl = string.Format(
CultureInfo.InvariantCulture,
_options.SmartTeachForumApiTemplate,
Uri.EscapeDataString(keyword),
requestCount);
requestCount,
Uri.EscapeDataString(sortToken));
requestUrl = UpsertHttpQueryParameter(requestUrl, "filter[q]", keyword);
requestUrl = UpsertHttpQueryParameter(requestUrl, "sort", sortToken);
requestUrl = UpsertHttpQueryParameter(
requestUrl,
"page[limit]",
requestCount.ToString(CultureInfo.InvariantCulture));
requestUrl = UpsertHttpQueryParameter(requestUrl, "include", "user");
using var request = new HttpRequestMessage(HttpMethod.Get, requestUrl);
request.Headers.TryAddWithoutValidation("User-Agent", UserAgent);
@@ -942,11 +955,94 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
return new Stcn24ForumPostsSnapshot(
Provider: "SmartTeachForum",
Source: "智教联盟论坛 STCN",
Source: ResolveStcn24ForumSourceLabel(sourceType),
Items: items,
FetchedAt: DateTimeOffset.UtcNow);
}
private static string ResolveSmartTeachDiscussionSortToken(string sourceType)
{
return Stcn24ForumSourceTypes.Normalize(sourceType) switch
{
Stcn24ForumSourceTypes.LatestCreated => "-createdAt",
Stcn24ForumSourceTypes.LatestActivity => "-lastPostedAt",
Stcn24ForumSourceTypes.MostReplies => "-commentCount",
Stcn24ForumSourceTypes.EarliestCreated => "createdAt",
Stcn24ForumSourceTypes.EarliestActivity => "lastPostedAt",
Stcn24ForumSourceTypes.LeastReplies => "commentCount",
Stcn24ForumSourceTypes.FrontpageLatest => "-frontdate",
Stcn24ForumSourceTypes.FrontpageEarliest => "frontdate",
_ => "-createdAt"
};
}
private static string ResolveStcn24ForumSourceLabel(string sourceType)
{
return Stcn24ForumSourceTypes.Normalize(sourceType) switch
{
Stcn24ForumSourceTypes.LatestCreated => "智教联盟论坛 STCN · 最新发布",
Stcn24ForumSourceTypes.LatestActivity => "智教联盟论坛 STCN · 最新回复",
Stcn24ForumSourceTypes.MostReplies => "智教联盟论坛 STCN · 回复最多",
Stcn24ForumSourceTypes.EarliestCreated => "智教联盟论坛 STCN · 最早发布",
Stcn24ForumSourceTypes.EarliestActivity => "智教联盟论坛 STCN · 最早回复",
Stcn24ForumSourceTypes.LeastReplies => "智教联盟论坛 STCN · 回复最少",
Stcn24ForumSourceTypes.FrontpageLatest => "智教联盟论坛 STCN · 前台推荐(新)",
Stcn24ForumSourceTypes.FrontpageEarliest => "智教联盟论坛 STCN · 前台推荐(旧)",
_ => "智教联盟论坛 STCN · 最新发布"
};
}
private static string UpsertHttpQueryParameter(string requestUrl, string key, string value)
{
if (!Uri.TryCreate(requestUrl, UriKind.Absolute, out var uri))
{
return requestUrl;
}
var parameters = new List<(string Key, string Value)>();
var replaced = false;
var query = uri.Query.TrimStart('?');
if (!string.IsNullOrWhiteSpace(query))
{
var parts = query.Split('&', StringSplitOptions.RemoveEmptyEntries);
foreach (var part in parts)
{
var separatorIndex = part.IndexOf('=');
var rawKey = separatorIndex >= 0 ? part[..separatorIndex] : part;
var rawValue = separatorIndex >= 0 ? part[(separatorIndex + 1)..] : string.Empty;
var normalizedKey = Uri.UnescapeDataString(rawKey);
if (string.Equals(normalizedKey, key, StringComparison.OrdinalIgnoreCase))
{
if (!replaced)
{
parameters.Add((key, value));
replaced = true;
}
continue;
}
parameters.Add((normalizedKey, Uri.UnescapeDataString(rawValue)));
}
}
if (!replaced)
{
parameters.Add((key, value));
}
var rebuiltQuery = string.Join(
"&",
parameters.Select(item =>
$"{Uri.EscapeDataString(item.Key)}={Uri.EscapeDataString(item.Value)}"));
var builder = new UriBuilder(uri)
{
Query = rebuiltQuery
};
return builder.Uri.ToString();
}
private async Task<string?> TryFetchBilibiliSearchPlaceholderAsync(CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(_options.BilibiliSearchDefaultApiUrl))
@@ -1049,26 +1145,31 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
return selection;
}
private bool TryGetStcn24ForumPostsFromCache(out Stcn24ForumPostsSnapshot snapshot)
private bool TryGetStcn24ForumPostsFromCache(string sourceType, out Stcn24ForumPostsSnapshot snapshot)
{
var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
lock (_cacheGate)
{
if (_stcn24ForumPostsCache is not null && _stcn24ForumPostsCache.ExpireAt > DateTimeOffset.UtcNow)
if (_stcn24ForumPostsCacheBySource.TryGetValue(normalizedSourceType, out var cacheEntry) &&
cacheEntry.ExpireAt > DateTimeOffset.UtcNow)
{
snapshot = _stcn24ForumPostsCache.Snapshot;
snapshot = cacheEntry.Snapshot;
return true;
}
_stcn24ForumPostsCacheBySource.Remove(normalizedSourceType);
}
snapshot = null!;
return false;
}
private void SetStcn24ForumPostsCache(Stcn24ForumPostsSnapshot snapshot)
private void SetStcn24ForumPostsCache(string sourceType, Stcn24ForumPostsSnapshot snapshot)
{
var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
lock (_cacheGate)
{
_stcn24ForumPostsCache = new Stcn24ForumPostsCacheEntry(
_stcn24ForumPostsCacheBySource[normalizedSourceType] = new Stcn24ForumPostsCacheEntry(
snapshot,
DateTimeOffset.UtcNow.Add(_options.CacheDuration));
}