diff --git a/LanMountainDesktop/App.axaml b/LanMountainDesktop/App.axaml
index b7099f2..3737feb 100644
--- a/LanMountainDesktop/App.axaml
+++ b/LanMountainDesktop/App.axaml
@@ -15,6 +15,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs
index 50ce6a6..dc391d8 100644
--- a/LanMountainDesktop/App.axaml.cs
+++ b/LanMountainDesktop/App.axaml.cs
@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins;
using System;
+using System.Diagnostics;
using System.Linq;
using Avalonia.Markup.Xaml;
using LanMountainDesktop.Services;
@@ -37,6 +38,57 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted();
}
+ private void OnTrayExitClick(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ desktop.Shutdown();
+ }
+ }
+
+ private void OnTrayRestartClick(object? sender, EventArgs e)
+ {
+ if (ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
+ {
+ return;
+ }
+
+ if (TryStartCurrentProcess())
+ {
+ desktop.Shutdown();
+ }
+ }
+
+ private static bool TryStartCurrentProcess()
+ {
+ try
+ {
+ var args = Environment.GetCommandLineArgs();
+ if (args.Length == 0 || string.IsNullOrWhiteSpace(args[0]))
+ {
+ return false;
+ }
+
+ var startInfo = new ProcessStartInfo
+ {
+ FileName = args[0],
+ UseShellExecute = false
+ };
+
+ for (var i = 1; i < args.Length; i++)
+ {
+ startInfo.ArgumentList.Add(args[i]);
+ }
+
+ Process.Start(startInfo);
+ return true;
+ }
+ catch
+ {
+ return false;
+ }
+ }
+
private void DisableAvaloniaDataAnnotationValidation()
{
// Get an array of plugins to remove
diff --git a/LanMountainDesktop/Localization/en-US.json b/LanMountainDesktop/Localization/en-US.json
index de3972d..c349baf 100644
--- a/LanMountainDesktop/Localization/en-US.json
+++ b/LanMountainDesktop/Localization/en-US.json
@@ -254,10 +254,11 @@
"launcher.empty_folder": "This folder is empty.",
"launcher.folder_items_format": "{0} apps",
"launcher.context.hide_icon": "Hide Icon",
+ "launcher.action.hide": "Hide",
"settings.launcher.title": "App Launcher",
"settings.launcher.hidden_header": "Hidden Items",
"settings.launcher.hidden_desc": "Review hidden launcher entries and show them again.",
- "settings.launcher.hidden_hint": "Right-click an icon in launcher to hide it. Hidden entries appear here.",
+ "settings.launcher.hidden_hint": "In desktop edit mode, select a launcher icon and click Hide. Hidden entries appear here.",
"settings.launcher.hidden_empty": "No hidden items.",
"settings.launcher.hidden_type_folder": "Folder",
"settings.launcher.hidden_type_shortcut": "Shortcut",
@@ -358,10 +359,63 @@
"bilihot.widget.fetch_failed": "Hot search fetch failed",
"bilihot.widget.fallback_item": "No hot search data",
"bilihot.widget.more_hot": "More hot search",
+ "dailyword.settings.title": "Daily word settings",
+ "dailyword.settings.desc": "Configure auto refresh and refresh interval.",
+ "dailyword.settings.auto_refresh_label": "Auto refresh",
+ "dailyword.settings.auto_refresh_enabled": "Enable auto refresh",
+ "dailyword.settings.frequency_label": "Refresh interval",
+ "bilihot.settings.title": "Bilibili hot search settings",
+ "bilihot.settings.desc": "Configure auto refresh and refresh interval.",
+ "bilihot.settings.auto_refresh_label": "Auto refresh",
+ "bilihot.settings.auto_refresh_enabled": "Enable auto refresh",
+ "bilihot.settings.frequency_label": "Refresh interval",
+ "refresh.frequency.5m": "5 minutes",
+ "refresh.frequency.10m": "10 minutes",
+ "refresh.frequency.12m": "12 minutes",
+ "refresh.frequency.15m": "15 minutes",
+ "refresh.frequency.20m": "20 minutes",
+ "refresh.frequency.30m": "30 minutes",
+ "refresh.frequency.40m": "40 minutes",
+ "refresh.frequency.1h": "1 hour",
+ "refresh.frequency.3h": "3 hours",
+ "refresh.frequency.6h": "6 hours",
+ "refresh.frequency.12h": "12 hours",
+ "refresh.frequency.24h": "24 hours",
+ "weather.widget.settings.title": "Weather widget settings",
+ "weather.widget.settings.desc": "Configure auto refresh and refresh interval for all weather widgets.",
+ "weather.widget.settings.auto_refresh_label": "Auto refresh",
+ "weather.widget.settings.auto_refresh_enabled": "Enable auto refresh",
+ "weather.widget.settings.frequency_label": "Refresh interval",
+ "weather.widget.settings.frequency_10m": "10 minutes",
+ "weather.widget.settings.frequency_12m": "12 minutes",
+ "weather.widget.settings.frequency_15m": "15 minutes",
+ "weather.widget.settings.frequency_30m": "30 minutes",
+ "weather.widget.settings.frequency_1h": "1 hour",
+ "weather.widget.settings.frequency_3h": "3 hours",
"stcn24.widget.loading": "Loading...",
"stcn24.widget.loading_item": "Loading...",
"stcn24.widget.fetch_failed": "Forum posts fetch failed",
"stcn24.widget.fallback_item": "No posts",
+ "stcn24.settings.title": "STCN 24 settings",
+ "stcn24.settings.desc": "Configure information source, auto refresh and refresh interval.",
+ "stcn24.settings.source_label": "Information source",
+ "stcn24.settings.source_latest_created": "Latest posts",
+ "stcn24.settings.source_latest_activity": "Latest activity",
+ "stcn24.settings.source_most_replies": "Most replies",
+ "stcn24.settings.source_earliest_created": "Earliest posts",
+ "stcn24.settings.source_earliest_activity": "Earliest activity",
+ "stcn24.settings.source_least_replies": "Least replies",
+ "stcn24.settings.source_frontpage_latest": "Frontpage latest",
+ "stcn24.settings.source_frontpage_earliest": "Frontpage earliest",
+ "stcn24.settings.auto_refresh_label": "Auto refresh",
+ "stcn24.settings.auto_refresh_enabled": "Enable auto refresh",
+ "stcn24.settings.frequency_label": "Refresh interval",
+ "stcn24.settings.frequency_5m": "5 minutes",
+ "stcn24.settings.frequency_10m": "10 minutes",
+ "stcn24.settings.frequency_20m": "20 minutes",
+ "stcn24.settings.frequency_30m": "30 minutes",
+ "stcn24.settings.frequency_1h": "1 hour",
+ "stcn24.settings.frequency_3h": "3 hours",
"exchange.widget.loading": "Loading exchange rates...",
"exchange.widget.fetch_failed": "Exchange rate fetch failed",
"cnrnews.settings.title": "CNR Settings",
diff --git a/LanMountainDesktop/Localization/zh-CN.json b/LanMountainDesktop/Localization/zh-CN.json
index 9ce94c7..89a2ee1 100644
--- a/LanMountainDesktop/Localization/zh-CN.json
+++ b/LanMountainDesktop/Localization/zh-CN.json
@@ -254,10 +254,11 @@
"launcher.empty_folder": "此文件夹为空。",
"launcher.folder_items_format": "{0} 个应用",
"launcher.context.hide_icon": "隐藏图标",
+ "launcher.action.hide": "隐藏",
"settings.launcher.title": "应用启动台",
"settings.launcher.hidden_header": "已隐藏项目",
"settings.launcher.hidden_desc": "查看已隐藏的启动台项目并重新显示。",
- "settings.launcher.hidden_hint": "在启动台中右键图标可隐藏,隐藏后的项目会显示在这里。",
+ "settings.launcher.hidden_hint": "进入桌面编辑模式后,在启动台选中图标并点击“隐藏”,隐藏后的项目会显示在这里。",
"settings.launcher.hidden_empty": "暂无隐藏项目。",
"settings.launcher.hidden_type_folder": "文件夹",
"settings.launcher.hidden_type_shortcut": "快捷方式",
@@ -358,10 +359,63 @@
"bilihot.widget.fetch_failed": "热搜获取失败",
"bilihot.widget.fallback_item": "暂无热搜",
"bilihot.widget.more_hot": "更多热搜",
+ "dailyword.settings.title": "每日单词设置",
+ "dailyword.settings.desc": "配置自动刷新开关与刷新频率。",
+ "dailyword.settings.auto_refresh_label": "自动刷新",
+ "dailyword.settings.auto_refresh_enabled": "启用自动刷新",
+ "dailyword.settings.frequency_label": "刷新频率",
+ "bilihot.settings.title": "B站热搜设置",
+ "bilihot.settings.desc": "配置自动刷新开关与刷新频率。",
+ "bilihot.settings.auto_refresh_label": "自动刷新",
+ "bilihot.settings.auto_refresh_enabled": "启用自动刷新",
+ "bilihot.settings.frequency_label": "刷新频率",
+ "refresh.frequency.5m": "5 分钟",
+ "refresh.frequency.10m": "10 分钟",
+ "refresh.frequency.12m": "12 分钟",
+ "refresh.frequency.15m": "15 分钟",
+ "refresh.frequency.20m": "20 分钟",
+ "refresh.frequency.30m": "30 分钟",
+ "refresh.frequency.40m": "40 分钟",
+ "refresh.frequency.1h": "1 小时",
+ "refresh.frequency.3h": "3 小时",
+ "refresh.frequency.6h": "6 小时",
+ "refresh.frequency.12h": "12 小时",
+ "refresh.frequency.24h": "24 小时",
+ "weather.widget.settings.title": "天气组件设置",
+ "weather.widget.settings.desc": "配置全部天气组件的自动刷新开关与刷新频率。",
+ "weather.widget.settings.auto_refresh_label": "自动刷新",
+ "weather.widget.settings.auto_refresh_enabled": "启用自动刷新",
+ "weather.widget.settings.frequency_label": "刷新频率",
+ "weather.widget.settings.frequency_10m": "10 分钟",
+ "weather.widget.settings.frequency_12m": "12 分钟",
+ "weather.widget.settings.frequency_15m": "15 分钟",
+ "weather.widget.settings.frequency_30m": "30 分钟",
+ "weather.widget.settings.frequency_1h": "1 小时",
+ "weather.widget.settings.frequency_3h": "3 小时",
"stcn24.widget.loading": "加载中...",
"stcn24.widget.loading_item": "加载中...",
"stcn24.widget.fetch_failed": "帖子获取失败",
"stcn24.widget.fallback_item": "暂无帖子",
+ "stcn24.settings.title": "STCN 24 设置",
+ "stcn24.settings.desc": "配置信息源、自动刷新开关与刷新频率。",
+ "stcn24.settings.source_label": "信息源",
+ "stcn24.settings.source_latest_created": "最新发布",
+ "stcn24.settings.source_latest_activity": "最新回复",
+ "stcn24.settings.source_most_replies": "回复最多",
+ "stcn24.settings.source_earliest_created": "最早发布",
+ "stcn24.settings.source_earliest_activity": "最早回复",
+ "stcn24.settings.source_least_replies": "回复最少",
+ "stcn24.settings.source_frontpage_latest": "前台推荐(新)",
+ "stcn24.settings.source_frontpage_earliest": "前台推荐(旧)",
+ "stcn24.settings.auto_refresh_label": "自动刷新",
+ "stcn24.settings.auto_refresh_enabled": "启用自动刷新",
+ "stcn24.settings.frequency_label": "刷新频率",
+ "stcn24.settings.frequency_5m": "5 分钟",
+ "stcn24.settings.frequency_10m": "10 分钟",
+ "stcn24.settings.frequency_20m": "20 分钟",
+ "stcn24.settings.frequency_30m": "30 分钟",
+ "stcn24.settings.frequency_1h": "1 小时",
+ "stcn24.settings.frequency_3h": "3 小时",
"exchange.widget.loading": "正在加载汇率...",
"exchange.widget.fetch_failed": "汇率获取失败",
"cnrnews.settings.title": "央广网设置",
diff --git a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
index 363fba5..c22d93e 100644
--- a/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
+++ b/LanMountainDesktop/Models/ComponentSettingsSnapshot.cs
@@ -40,6 +40,16 @@ public sealed class ComponentSettingsSnapshot
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;
+
public ComponentSettingsSnapshot Clone()
{
var clone = (ComponentSettingsSnapshot)MemberwiseClone();
diff --git a/LanMountainDesktop/Models/RefreshIntervalCatalog.cs b/LanMountainDesktop/Models/RefreshIntervalCatalog.cs
new file mode 100644
index 0000000..c200d37
--- /dev/null
+++ b/LanMountainDesktop/Models/RefreshIntervalCatalog.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace LanMountainDesktop.Models;
+
+public static class RefreshIntervalCatalog
+{
+ public static IReadOnlyList SupportedIntervalsMinutes { get; } =
+ [
+ 5,
+ 10,
+ 12,
+ 15,
+ 20,
+ 30,
+ 40,
+ 60,
+ 180,
+ 360,
+ 720,
+ 1440
+ ];
+
+ public static int Normalize(int minutes, int fallbackMinutes)
+ {
+ if (minutes <= 0)
+ {
+ return fallbackMinutes;
+ }
+
+ if (SupportedIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(fallbackMinutes);
+ }
+
+ public static string ToLocalizationKeySuffix(int minutes)
+ {
+ return minutes switch
+ {
+ 5 => "5m",
+ 10 => "10m",
+ 12 => "12m",
+ 15 => "15m",
+ 20 => "20m",
+ 30 => "30m",
+ 40 => "40m",
+ 60 => "1h",
+ 180 => "3h",
+ 360 => "6h",
+ 720 => "12h",
+ 1440 => "24h",
+ _ => $"{minutes}m"
+ };
+ }
+
+ public static string ToEnglishFallbackLabel(int minutes)
+ {
+ return minutes switch
+ {
+ 60 => "1 hour",
+ 180 => "3 hours",
+ 360 => "6 hours",
+ 720 => "12 hours",
+ 1440 => "24 hours",
+ _ => $"{minutes} min"
+ };
+ }
+}
diff --git a/LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs b/LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs
new file mode 100644
index 0000000..e6f12bb
--- /dev/null
+++ b/LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs
@@ -0,0 +1,42 @@
+using System;
+using System.Collections.Generic;
+
+namespace LanMountainDesktop.Models;
+
+public static class Stcn24ForumSourceTypes
+{
+ public const string LatestCreated = "LatestCreated";
+ public const string LatestActivity = "LatestActivity";
+ public const string MostReplies = "MostReplies";
+ public const string EarliestCreated = "EarliestCreated";
+ public const string EarliestActivity = "EarliestActivity";
+ public const string LeastReplies = "LeastReplies";
+ public const string FrontpageLatest = "FrontpageLatest";
+ public const string FrontpageEarliest = "FrontpageEarliest";
+
+ public static IReadOnlyList SupportedValues { get; } =
+ [
+ LatestCreated,
+ LatestActivity,
+ MostReplies,
+ EarliestCreated,
+ EarliestActivity,
+ LeastReplies,
+ FrontpageLatest,
+ FrontpageEarliest
+ ];
+
+ public static string Normalize(string? value)
+ {
+ var candidate = value?.Trim() ?? string.Empty;
+ foreach (var supported in SupportedValues)
+ {
+ if (string.Equals(candidate, supported, StringComparison.OrdinalIgnoreCase))
+ {
+ return supported;
+ }
+ }
+
+ return LatestCreated;
+ }
+}
diff --git a/LanMountainDesktop/Models/TaskbarActionId.cs b/LanMountainDesktop/Models/TaskbarActionId.cs
index 227dced..c421bd6 100644
--- a/LanMountainDesktop/Models/TaskbarActionId.cs
+++ b/LanMountainDesktop/Models/TaskbarActionId.cs
@@ -7,5 +7,6 @@ public enum TaskbarActionId
AddDesktopPage,
DeleteDesktopPage,
DeleteComponent,
- EditComponent
+ EditComponent,
+ HideLauncherEntry
}
diff --git a/LanMountainDesktop/Services/ComponentSettingsService.cs b/LanMountainDesktop/Services/ComponentSettingsService.cs
index 14010d4..787664c 100644
--- a/LanMountainDesktop/Services/ComponentSettingsService.cs
+++ b/LanMountainDesktop/Services/ComponentSettingsService.cs
@@ -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;
}
}
diff --git a/LanMountainDesktop/Services/IRecommendationDataService.cs b/LanMountainDesktop/Services/IRecommendationDataService.cs
index 742c2a2..0ac629a 100644
--- a/LanMountainDesktop/Services/IRecommendationDataService.cs
+++ b/LanMountainDesktop/Services/IRecommendationDataService.cs
@@ -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(
diff --git a/LanMountainDesktop/Services/RecommendationDataService.cs b/LanMountainDesktop/Services/RecommendationDataService.cs
index c18cb69..602ab21 100644
--- a/LanMountainDesktop/Services/RecommendationDataService.cs
+++ b/LanMountainDesktop/Services/RecommendationDataService.cs
@@ -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 _stcn24ForumPostsCacheBySource =
+ new(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary _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.Fail(
@@ -373,7 +375,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
"No STCN forum posts were returned.");
}
- SetStcn24ForumPostsCache(snapshot);
+ SetStcn24ForumPostsCache(sourceType, snapshot);
return RecommendationQueryResult.Ok(snapshot);
}
catch (OperationCanceledException)
@@ -814,6 +816,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
private async Task 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 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));
}
diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs
index b7861b8..b596daa 100644
--- a/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchSettingsWindow.axaml.cs
@@ -1,15 +1,17 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class BilibiliHotSearchSettingsWindow : UserControl
{
- private static readonly int[] SupportedIntervals = [5, 10, 15, 30, 60, 180];
+ private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
public BilibiliHotSearchSettingsWindow()
{
InitializeComponent();
+ InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
@@ -49,12 +52,7 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
AutoRefreshLabelTextBlock.Text = L("bilihot.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("bilihot.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("bilihot.settings.frequency_label", "Refresh interval");
- Frequency5mItem.Content = L("bilihot.settings.frequency_5m", "5 min");
- Frequency10mItem.Content = L("bilihot.settings.frequency_10m", "10 min");
- Frequency15mItem.Content = L("bilihot.settings.frequency_15m", "15 min");
- Frequency30mItem.Content = L("bilihot.settings.frequency_30m", "30 min");
- Frequency1hItem.Content = L("bilihot.settings.frequency_1h", "1 hour");
- Frequency3hItem.Content = L("bilihot.settings.frequency_3h", "3 hours");
+ ApplyFrequencyLocalization();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
private static int NormalizeInterval(int minutes)
{
- if (minutes <= 0)
- {
- return 15;
- }
+ return RefreshIntervalCatalog.Normalize(minutes, 15);
+ }
- if (SupportedIntervals.Contains(minutes))
+ private void InitializeFrequencyOptions()
+ {
+ FrequencyComboBox.Items.Clear();
+ foreach (var minutes in SupportedIntervals)
{
- return minutes;
+ FrequencyComboBox.Items.Add(new ComboBoxItem
+ {
+ Tag = minutes.ToString(),
+ Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
+ });
}
+ }
- return SupportedIntervals
- .OrderBy(value => Math.Abs(value - minutes))
- .FirstOrDefault(15);
+ private void ApplyFrequencyLocalization()
+ {
+ foreach (var item in FrequencyComboBox.Items.OfType())
+ {
+ if (item.Tag is not string tagText ||
+ !int.TryParse(tagText, out var minutes))
+ {
+ continue;
+ }
+
+ var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
+ item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
+ }
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
index 40dc992..a8c5f17 100644
--- a/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/BilibiliHotSearchWidget.axaml.cs
@@ -25,7 +25,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2;
private const int MaxDisplayItemCount = 4;
- private static readonly int[] SupportedAutoRefreshIntervalsMinutes = [5, 10, 15, 30, 60, 180];
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs
index dfc8b13..f0d0de1 100644
--- a/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/Components/CnrDailyNewsSettingsWindow.axaml.cs
@@ -1,15 +1,17 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class CnrDailyNewsSettingsWindow : UserControl
{
- private static readonly int[] SupportedIntervals = [5, 10, 40, 60, 720, 1440];
+ private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
public CnrDailyNewsSettingsWindow()
{
InitializeComponent();
+ InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
@@ -49,12 +52,7 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation");
AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation");
FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval");
- Frequency5mItem.Content = L("cnrnews.settings.frequency_5m", "5 min");
- Frequency10mItem.Content = L("cnrnews.settings.frequency_10m", "10 min");
- Frequency40mItem.Content = L("cnrnews.settings.frequency_40m", "40 min");
- Frequency1hItem.Content = L("cnrnews.settings.frequency_1h", "1 hour");
- Frequency12hItem.Content = L("cnrnews.settings.frequency_12h", "12 hours");
- Frequency24hItem.Content = L("cnrnews.settings.frequency_24h", "24 hours");
+ ApplyFrequencyLocalization();
}
private void OnAutoRotateChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
private static int NormalizeInterval(int minutes)
{
- if (minutes <= 0)
- {
- return 60;
- }
+ return RefreshIntervalCatalog.Normalize(minutes, 60);
+ }
- if (SupportedIntervals.Contains(minutes))
+ private void InitializeFrequencyOptions()
+ {
+ FrequencyComboBox.Items.Clear();
+ foreach (var minutes in SupportedIntervals)
{
- return minutes;
+ FrequencyComboBox.Items.Add(new ComboBoxItem
+ {
+ Tag = minutes.ToString(),
+ Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
+ });
}
+ }
- return SupportedIntervals
- .OrderBy(value => Math.Abs(value - minutes))
- .FirstOrDefault(60);
+ private void ApplyFrequencyLocalization()
+ {
+ foreach (var item in FrequencyComboBox.Items.OfType())
+ {
+ if (item.Tag is not string tagText ||
+ !int.TryParse(tagText, out var minutes))
+ {
+ continue;
+ }
+
+ var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
+ item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
+ }
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
index 00f2018..a685312 100644
--- a/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/CnrDailyNewsWidget.axaml.cs
@@ -36,7 +36,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private const double BaseCellSize = 48d;
private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2;
- private static readonly int[] SupportedAutoRotateIntervalsMinutes = [5, 10, 40, 60, 720, 1440];
+ private static readonly IReadOnlyList SupportedAutoRotateIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
diff --git a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs
index ec316d3..4e0d80a 100644
--- a/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWordSettingsWindow.axaml.cs
@@ -1,15 +1,17 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components;
public partial class DailyWordSettingsWindow : UserControl
{
- private static readonly int[] SupportedIntervals = [30, 60, 180, 360, 720, 1440];
+ private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class DailyWordSettingsWindow : UserControl
public DailyWordSettingsWindow()
{
InitializeComponent();
+ InitializeFrequencyOptions();
LoadState();
ApplyLocalization();
}
@@ -49,12 +52,7 @@ public partial class DailyWordSettingsWindow : UserControl
AutoRefreshLabelTextBlock.Text = L("dailyword.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("dailyword.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("dailyword.settings.frequency_label", "Refresh interval");
- Frequency30mItem.Content = L("dailyword.settings.frequency_30m", "30 min");
- Frequency1hItem.Content = L("dailyword.settings.frequency_1h", "1 hour");
- Frequency3hItem.Content = L("dailyword.settings.frequency_3h", "3 hours");
- Frequency6hItem.Content = L("dailyword.settings.frequency_6h", "6 hours");
- Frequency12hItem.Content = L("dailyword.settings.frequency_12h", "12 hours");
- Frequency24hItem.Content = L("dailyword.settings.frequency_24h", "24 hours");
+ ApplyFrequencyLocalization();
}
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class DailyWordSettingsWindow : UserControl
private static int NormalizeInterval(int minutes)
{
- if (minutes <= 0)
- {
- return 360;
- }
+ return RefreshIntervalCatalog.Normalize(minutes, 360);
+ }
- if (SupportedIntervals.Contains(minutes))
+ private void InitializeFrequencyOptions()
+ {
+ FrequencyComboBox.Items.Clear();
+ foreach (var minutes in SupportedIntervals)
{
- return minutes;
+ FrequencyComboBox.Items.Add(new ComboBoxItem
+ {
+ Tag = minutes.ToString(),
+ Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
+ });
}
+ }
- return SupportedIntervals
- .OrderBy(value => Math.Abs(value - minutes))
- .FirstOrDefault(360);
+ private void ApplyFrequencyLocalization()
+ {
+ foreach (var item in FrequencyComboBox.Items.OfType())
+ {
+ if (item.Tag is not string tagText ||
+ !int.TryParse(tagText, out var minutes))
+ {
+ continue;
+ }
+
+ var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
+ item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
+ }
}
private string L(string key, string fallback)
diff --git a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
index bd799b6..0785c33 100644
--- a/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/DailyWordWidget.axaml.cs
@@ -1,4 +1,5 @@
using System;
+using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading;
@@ -22,7 +23,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
private const double BaseCellSize = 48d;
private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2;
- private static readonly int[] SupportedAutoRefreshIntervalsMinutes = [30, 60, 180, 360, 720, 1440];
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
diff --git a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
index 48d7dea..bfb6cbc 100644
--- a/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/ExtendedWeatherWidget.axaml.cs
@@ -18,12 +18,14 @@ namespace LanMountainDesktop.Views.Components;
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
{
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly AppSettingsService _settingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
@@ -34,6 +36,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isAttached;
private bool _isOnActivePage = true;
private bool _isRefreshing;
+ private bool _autoRefreshEnabled = true;
private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.ClearDay;
private readonly TextBlock[] _hourlyTempBlocks;
@@ -87,6 +90,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
ApplyCellSize(_currentCellSize);
ApplyVisualTheme(_activeVisualKind);
ApplyFallback();
+ ApplyAutoRefreshSettings();
}
private void ConfigureTextOverflowGuards()
@@ -160,6 +164,15 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
}
}
+ public void RefreshFromSettings()
+ {
+ ApplyAutoRefreshSettings();
+ if (_isAttached && _isOnActivePage)
+ {
+ _ = RefreshWeatherAsync(forceRefresh: true);
+ }
+ }
+
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -184,6 +197,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
+ ApplyAutoRefreshSettings();
UpdateTimerState();
if (_isOnActivePage)
{
@@ -893,10 +907,14 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{
if (_isAttached && _isOnActivePage)
{
- if (!_refreshTimer.IsEnabled)
+ if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{
_refreshTimer.Start();
}
+ else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Stop();
+ }
if (!_animationTimer.IsEnabled)
{
@@ -910,6 +928,48 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
_animationTimer.Stop();
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 12;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ enabled = snapshot.WeatherAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ }
+
+ _autoRefreshEnabled = enabled;
+ _refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+
+ if (_isAttached)
+ {
+ UpdateTimerState();
+ }
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 12;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(12);
+ }
+
private void CancelRefresh()
{
var cts = Interlocked.Exchange(ref _refreshCts, null);
diff --git a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
index e7489f1..0b1ecb3 100644
--- a/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/HourlyWeatherWidget.axaml.cs
@@ -82,6 +82,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
@@ -94,6 +95,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _settingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
@@ -115,6 +117,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private bool _isAttached;
private bool _isOnActivePage = true;
private bool _isRefreshing;
+ private bool _autoRefreshEnabled = true;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
@@ -147,6 +150,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize);
+ ApplyAutoRefreshSettings();
}
private void ConfigureTextOverflowGuards()
@@ -211,6 +215,15 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
}
}
+ public void RefreshFromSettings()
+ {
+ ApplyAutoRefreshSettings();
+ if (_isAttached && _isOnActivePage)
+ {
+ _ = RefreshWeatherAsync(forceRefresh: true);
+ }
+ }
+
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -249,6 +262,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
+ ApplyAutoRefreshSettings();
UpdateTimerState();
if (_isOnActivePage)
{
@@ -1382,10 +1396,14 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{
if (_isAttached && _isOnActivePage)
{
- if (!_refreshTimer.IsEnabled)
+ if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{
_refreshTimer.Start();
}
+ else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Stop();
+ }
if (!_backgroundAnimationTimer.IsEnabled)
{
@@ -1399,6 +1417,48 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
_backgroundAnimationTimer.Stop();
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 12;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ enabled = snapshot.WeatherAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ }
+
+ _autoRefreshEnabled = enabled;
+ _refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+
+ if (_isAttached)
+ {
+ UpdateTimerState();
+ }
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 12;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(12);
+ }
+
private void InitializeParticleVisuals()
{
if (_particleVisuals.Count > 0)
diff --git a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
index 62ae895..171417e 100644
--- a/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/MultiDayWeatherWidget.axaml.cs
@@ -80,6 +80,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
@@ -92,6 +93,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
};
private readonly AppSettingsService _settingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
@@ -113,6 +115,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isAttached;
private bool _isOnActivePage = true;
private bool _isRefreshing;
+ private bool _autoRefreshEnabled = true;
private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks;
@@ -145,6 +148,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize);
+ ApplyAutoRefreshSettings();
}
private void ConfigureTextOverflowGuards()
@@ -209,6 +213,15 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
}
}
+ public void RefreshFromSettings()
+ {
+ ApplyAutoRefreshSettings();
+ if (_isAttached && _isOnActivePage)
+ {
+ _ = RefreshWeatherAsync(forceRefresh: true);
+ }
+ }
+
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -247,6 +260,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
+ ApplyAutoRefreshSettings();
UpdateTimerState();
if (_isOnActivePage)
{
@@ -1232,10 +1246,14 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{
if (_isAttached && _isOnActivePage)
{
- if (!_refreshTimer.IsEnabled)
+ if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{
_refreshTimer.Start();
}
+ else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Stop();
+ }
if (!_backgroundAnimationTimer.IsEnabled)
{
@@ -1249,6 +1267,48 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_backgroundAnimationTimer.Stop();
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 12;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ enabled = snapshot.WeatherAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ }
+
+ _autoRefreshEnabled = enabled;
+ _refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+
+ if (_isAttached)
+ {
+ UpdateTimerState();
+ }
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 12;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(12);
+ }
+
private void InitializeParticleVisuals()
{
if (_particleVisuals.Count > 0)
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml
new file mode 100644
index 0000000..c93cfbc
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml
@@ -0,0 +1,128 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs
new file mode 100644
index 0000000..7c59334
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs
@@ -0,0 +1,199 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services;
+
+namespace LanMountainDesktop.Views.Components;
+
+public partial class Stcn24ForumSettingsWindow : UserControl
+{
+ private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
+
+ private readonly AppSettingsService _appSettingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
+ private readonly LocalizationService _localizationService = new();
+ private bool _suppressEvents;
+ private string _languageCode = "zh-CN";
+
+ public event EventHandler? SettingsChanged;
+
+ public Stcn24ForumSettingsWindow()
+ {
+ InitializeComponent();
+ InitializeFrequencyOptions();
+ LoadState();
+ ApplyLocalization();
+ }
+
+ private void LoadState()
+ {
+ var appSnapshot = _appSettingsService.Load();
+ var componentSnapshot = _componentSettingsService.Load();
+ _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
+
+ var enabled = componentSnapshot.Stcn24ForumAutoRefreshEnabled;
+ var interval = NormalizeInterval(componentSnapshot.Stcn24ForumAutoRefreshIntervalMinutes);
+ var sourceType = Stcn24ForumSourceTypes.Normalize(componentSnapshot.Stcn24ForumSourceType);
+
+ _suppressEvents = true;
+ AutoRefreshCheckBox.IsChecked = enabled;
+ SelectSourceType(sourceType);
+ SelectInterval(interval);
+ FrequencyCardBorder.IsVisible = enabled;
+ _suppressEvents = false;
+ }
+
+ private void ApplyLocalization()
+ {
+ TitleTextBlock.Text = L("stcn24.settings.title", "STCN 24 settings");
+ DescriptionTextBlock.Text = L("stcn24.settings.desc", "Configure information source, auto refresh and refresh interval.");
+ SourceLabelTextBlock.Text = L("stcn24.settings.source_label", "Information source");
+ SourceLatestCreatedItem.Content = L("stcn24.settings.source_latest_created", "Latest posts");
+ SourceLatestActivityItem.Content = L("stcn24.settings.source_latest_activity", "Latest activity");
+ SourceMostRepliesItem.Content = L("stcn24.settings.source_most_replies", "Most replies");
+ SourceEarliestCreatedItem.Content = L("stcn24.settings.source_earliest_created", "Earliest posts");
+ SourceEarliestActivityItem.Content = L("stcn24.settings.source_earliest_activity", "Earliest activity");
+ SourceLeastRepliesItem.Content = L("stcn24.settings.source_least_replies", "Least replies");
+ SourceFrontpageLatestItem.Content = L("stcn24.settings.source_frontpage_latest", "Frontpage latest");
+ SourceFrontpageEarliestItem.Content = L("stcn24.settings.source_frontpage_earliest", "Frontpage earliest");
+ AutoRefreshLabelTextBlock.Text = L("stcn24.settings.auto_refresh_label", "Auto refresh");
+ AutoRefreshCheckBox.Content = L("stcn24.settings.auto_refresh_enabled", "Enable auto refresh");
+ FrequencyLabelTextBlock.Text = L("stcn24.settings.frequency_label", "Refresh interval");
+ ApplyFrequencyLocalization();
+ }
+
+ private void OnSourceSelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ SaveState();
+ }
+
+ private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ var enabled = AutoRefreshCheckBox.IsChecked == true;
+ FrequencyCardBorder.IsVisible = enabled;
+ SaveState();
+ }
+
+ private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ SaveState();
+ }
+
+ private void SaveState()
+ {
+ var snapshot = _componentSettingsService.Load();
+ snapshot.Stcn24ForumSourceType = GetSelectedSourceType();
+ snapshot.Stcn24ForumAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
+ snapshot.Stcn24ForumAutoRefreshIntervalMinutes = GetSelectedInterval();
+ _componentSettingsService.Save(snapshot);
+ SettingsChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private string GetSelectedSourceType()
+ {
+ if (SourceComboBox.SelectedItem is ComboBoxItem item &&
+ item.Tag is string sourceTag)
+ {
+ return Stcn24ForumSourceTypes.Normalize(sourceTag);
+ }
+
+ return Stcn24ForumSourceTypes.LatestCreated;
+ }
+
+ private int GetSelectedInterval()
+ {
+ if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
+ item.Tag is string tagText &&
+ int.TryParse(tagText, out var minutes))
+ {
+ return NormalizeInterval(minutes);
+ }
+
+ return 20;
+ }
+
+ private void SelectInterval(int intervalMinutes)
+ {
+ var selected = FrequencyComboBox.Items
+ .OfType()
+ .FirstOrDefault(item =>
+ item.Tag is string tagText &&
+ int.TryParse(tagText, out var minutes) &&
+ minutes == intervalMinutes);
+ FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault();
+ }
+
+ private void SelectSourceType(string sourceType)
+ {
+ var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
+ var selected = SourceComboBox.Items
+ .OfType()
+ .FirstOrDefault(item =>
+ item.Tag is string sourceTag &&
+ string.Equals(Stcn24ForumSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase));
+ SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType().FirstOrDefault();
+ }
+
+ private static int NormalizeInterval(int minutes)
+ {
+ return RefreshIntervalCatalog.Normalize(minutes, 20);
+ }
+
+ private void InitializeFrequencyOptions()
+ {
+ FrequencyComboBox.Items.Clear();
+ foreach (var minutes in SupportedIntervals)
+ {
+ FrequencyComboBox.Items.Add(new ComboBoxItem
+ {
+ Tag = minutes.ToString(),
+ Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
+ });
+ }
+ }
+
+ private void ApplyFrequencyLocalization()
+ {
+ foreach (var item in FrequencyComboBox.Items.OfType())
+ {
+ if (item.Tag is not string tagText ||
+ !int.TryParse(tagText, out var minutes))
+ {
+ continue;
+ }
+
+ var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
+ item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
+ }
+ }
+
+ private string L(string key, string fallback)
+ {
+ return _localizationService.GetString(_languageCode, key, fallback);
+ }
+}
diff --git a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
index 59e8b3e..b1f2bd7 100644
--- a/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/Stcn24ForumWidget.axaml.cs
@@ -36,6 +36,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
private const int BaseWidthCells = 4;
private const int BaseHeightCells = 4;
private const int MaxDisplayItemCount = 4;
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
@@ -43,6 +44,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
};
private readonly AppSettingsService _appSettingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly List _activeItems = [];
private readonly List _itemVisuals = [];
@@ -51,9 +53,11 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
private CancellationTokenSource? _refreshCts;
private string _languageCode = "zh-CN";
+ private string _sourceType = Stcn24ForumSourceTypes.LatestCreated;
private double _currentCellSize = BaseCellSize;
private bool _isAttached;
private bool _isRefreshing;
+ private bool _autoRefreshEnabled = true;
private sealed record ForumItemVisual(
Border Host,
@@ -114,6 +118,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
ApplyCellSize(_currentCellSize);
UpdateLanguageCode();
+ ApplyAutoRefreshSettings();
ApplyLoadingState();
UpdateInteractionState();
UpdateRefreshButtonState();
@@ -134,13 +139,20 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
}
}
+ public void RefreshFromSettings()
+ {
+ _recommendationService.ClearCache();
+ ApplyAutoRefreshSettings();
+ if (_isAttached)
+ {
+ _ = RefreshPostsAsync(forceRefresh: true);
+ }
+ }
+
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
- if (!_refreshTimer.IsEnabled)
- {
- _refreshTimer.Start();
- }
+ ApplyAutoRefreshSettings();
_ = RefreshPostsAsync(forceRefresh: false);
}
@@ -211,6 +223,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
var query = new Stcn24ForumPostsQuery(
Locale: _languageCode,
ItemCount: MaxDisplayItemCount,
+ SourceType: _sourceType,
ForceRefresh: forceRefresh);
var result = await _recommendationService.GetStcn24ForumPostsAsync(query, cts.Token);
if (!_isAttached || cts.IsCancellationRequested)
@@ -392,6 +405,62 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
}
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 20;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ _sourceType = Stcn24ForumSourceTypes.Normalize(snapshot.Stcn24ForumSourceType);
+ enabled = snapshot.Stcn24ForumAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.Stcn24ForumAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ _sourceType = Stcn24ForumSourceTypes.LatestCreated;
+ }
+
+ _autoRefreshEnabled = enabled;
+ _refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+
+ if (!_isAttached)
+ {
+ return;
+ }
+
+ if (_autoRefreshEnabled)
+ {
+ if (!_refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Start();
+ }
+ }
+ else if (_refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Stop();
+ }
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 20;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(20);
+ }
+
private void UpdateAdaptiveLayout()
{
var scale = ResolveScale();
diff --git a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
index c944641..307b5d1 100644
--- a/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherClockWidget.axaml.cs
@@ -1,5 +1,7 @@
using System;
+using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia;
@@ -26,6 +28,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private const double DialCenter = DialDesignSize / 2d;
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _clockTimer = new()
{
@@ -38,6 +41,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
};
private readonly AppSettingsService _settingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);
@@ -51,6 +55,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private bool _dialInitialized;
private bool _handsInitialized;
private bool _isRefreshing;
+ private bool _weatherAutoRefreshEnabled = true;
private bool? _isNightModeApplied;
private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay;
@@ -70,6 +75,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
ApplyCellSize(_currentCellSize);
ApplyDefaultWeatherIcon();
UpdateClockVisual();
+ ApplyAutoRefreshSettings();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
@@ -100,6 +106,15 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
}
}
+ public void RefreshFromSettings()
+ {
+ ApplyAutoRefreshSettings();
+ if (_isAttached)
+ {
+ _ = RefreshWeatherAsync(forceRefresh: true);
+ }
+ }
+
public void ApplyCellSize(double cellSize)
{
_currentCellSize = Math.Max(1, cellSize);
@@ -203,9 +218,10 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
+ ApplyAutoRefreshSettings();
UpdateClockVisual();
_clockTimer.Start();
- _weatherRefreshTimer.Start();
+ UpdateWeatherRefreshTimerState();
_ = RefreshWeatherAsync(forceRefresh: false);
}
@@ -629,6 +645,59 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
return Math.Clamp(value, -180, 180);
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 12;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ enabled = snapshot.WeatherAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ }
+
+ _weatherAutoRefreshEnabled = enabled;
+ _weatherRefreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+ UpdateWeatherRefreshTimerState();
+ }
+
+ private void UpdateWeatherRefreshTimerState()
+ {
+ if (_isAttached && _weatherAutoRefreshEnabled)
+ {
+ if (!_weatherRefreshTimer.IsEnabled)
+ {
+ _weatherRefreshTimer.Start();
+ }
+
+ return;
+ }
+
+ _weatherRefreshTimer.Stop();
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 12;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(12);
+ }
+
private void CancelRefreshRequest()
{
var cts = Interlocked.Exchange(ref _refreshCts, null);
diff --git a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
index b3c03ab..3aeb597 100644
--- a/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
+++ b/LanMountainDesktop/Views/Components/WeatherWidget.axaml.cs
@@ -76,6 +76,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
double Longitude);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
+ private static readonly IReadOnlyList SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new()
{
@@ -88,6 +89,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
};
private readonly AppSettingsService _settingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly Dictionary _backgroundBrushCache = new();
private readonly Dictionary _particleBrushCache = new();
@@ -109,6 +111,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private bool _isAttached;
private bool _isOnActivePage = true;
private bool _isRefreshing;
+ private bool _autoRefreshEnabled = true;
public WeatherWidget()
{
@@ -125,6 +128,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize);
+ ApplyAutoRefreshSettings();
}
public void SetTimeZoneService(TimeZoneService timeZoneService)
@@ -154,6 +158,15 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
}
}
+ public void RefreshFromSettings()
+ {
+ ApplyAutoRefreshSettings();
+ if (_isAttached && _isOnActivePage)
+ {
+ _ = RefreshWeatherAsync(forceRefresh: true);
+ }
+ }
+
public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{
_ = isEditMode;
@@ -194,6 +207,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_isAttached = true;
+ ApplyAutoRefreshSettings();
UpdateTimerState();
if (_isOnActivePage)
{
@@ -1021,10 +1035,14 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{
if (_isAttached && _isOnActivePage)
{
- if (!_refreshTimer.IsEnabled)
+ if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{
_refreshTimer.Start();
}
+ else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
+ {
+ _refreshTimer.Stop();
+ }
if (!_backgroundAnimationTimer.IsEnabled)
{
@@ -1038,6 +1056,48 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
_backgroundAnimationTimer.Stop();
}
+ private void ApplyAutoRefreshSettings()
+ {
+ var enabled = true;
+ var intervalMinutes = 12;
+
+ try
+ {
+ var snapshot = _componentSettingsService.Load();
+ enabled = snapshot.WeatherAutoRefreshEnabled;
+ intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.WeatherAutoRefreshIntervalMinutes);
+ }
+ catch
+ {
+ // Keep fallback defaults.
+ }
+
+ _autoRefreshEnabled = enabled;
+ _refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
+
+ if (_isAttached)
+ {
+ UpdateTimerState();
+ }
+ }
+
+ private static int NormalizeAutoRefreshIntervalMinutes(int minutes)
+ {
+ if (minutes <= 0)
+ {
+ return 12;
+ }
+
+ if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
+ {
+ return minutes;
+ }
+
+ return SupportedAutoRefreshIntervalsMinutes
+ .OrderBy(value => Math.Abs(value - minutes))
+ .FirstOrDefault(12);
+ }
+
private void InitializeParticleVisuals()
{
if (_particleVisuals.Count > 0)
diff --git a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml
new file mode 100644
index 0000000..c8209bc
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml
@@ -0,0 +1,87 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs
new file mode 100644
index 0000000..e6fbfe4
--- /dev/null
+++ b/LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs
@@ -0,0 +1,153 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
+using LanMountainDesktop.Models;
+using LanMountainDesktop.Services;
+
+namespace LanMountainDesktop.Views.Components;
+
+public partial class WeatherWidgetSettingsWindow : UserControl
+{
+ private static readonly IReadOnlyList SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
+
+ private readonly AppSettingsService _appSettingsService = new();
+ private readonly ComponentSettingsService _componentSettingsService = new();
+ private readonly LocalizationService _localizationService = new();
+ private bool _suppressEvents;
+ private string _languageCode = "zh-CN";
+
+ public event EventHandler? SettingsChanged;
+
+ public WeatherWidgetSettingsWindow()
+ {
+ InitializeComponent();
+ InitializeFrequencyOptions();
+ LoadState();
+ ApplyLocalization();
+ }
+
+ private void LoadState()
+ {
+ var appSnapshot = _appSettingsService.Load();
+ var componentSnapshot = _componentSettingsService.Load();
+ _languageCode = _localizationService.NormalizeLanguageCode(appSnapshot.LanguageCode);
+
+ var enabled = componentSnapshot.WeatherAutoRefreshEnabled;
+ var interval = NormalizeInterval(componentSnapshot.WeatherAutoRefreshIntervalMinutes);
+
+ _suppressEvents = true;
+ AutoRefreshCheckBox.IsChecked = enabled;
+ SelectInterval(interval);
+ FrequencyCardBorder.IsVisible = enabled;
+ _suppressEvents = false;
+ }
+
+ private void ApplyLocalization()
+ {
+ TitleTextBlock.Text = L("weather.widget.settings.title", "Weather widget settings");
+ DescriptionTextBlock.Text = L("weather.widget.settings.desc", "Configure auto refresh and refresh interval for all weather widgets.");
+ AutoRefreshLabelTextBlock.Text = L("weather.widget.settings.auto_refresh_label", "Auto refresh");
+ AutoRefreshCheckBox.Content = L("weather.widget.settings.auto_refresh_enabled", "Enable auto refresh");
+ FrequencyLabelTextBlock.Text = L("weather.widget.settings.frequency_label", "Refresh interval");
+ ApplyFrequencyLocalization();
+ }
+
+ private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ var enabled = AutoRefreshCheckBox.IsChecked == true;
+ FrequencyCardBorder.IsVisible = enabled;
+ SaveState();
+ }
+
+ private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
+ {
+ _ = sender;
+ _ = e;
+ if (_suppressEvents)
+ {
+ return;
+ }
+
+ SaveState();
+ }
+
+ private void SaveState()
+ {
+ var snapshot = _componentSettingsService.Load();
+ snapshot.WeatherAutoRefreshEnabled = AutoRefreshCheckBox.IsChecked == true;
+ snapshot.WeatherAutoRefreshIntervalMinutes = GetSelectedInterval();
+ _componentSettingsService.Save(snapshot);
+ SettingsChanged?.Invoke(this, EventArgs.Empty);
+ }
+
+ private int GetSelectedInterval()
+ {
+ if (FrequencyComboBox.SelectedItem is ComboBoxItem item &&
+ item.Tag is string tagText &&
+ int.TryParse(tagText, out var minutes))
+ {
+ return NormalizeInterval(minutes);
+ }
+
+ return 12;
+ }
+
+ private void SelectInterval(int intervalMinutes)
+ {
+ var selected = FrequencyComboBox.Items
+ .OfType()
+ .FirstOrDefault(item =>
+ item.Tag is string tagText &&
+ int.TryParse(tagText, out var minutes) &&
+ minutes == intervalMinutes);
+ FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType().FirstOrDefault();
+ }
+
+ private static int NormalizeInterval(int minutes)
+ {
+ return RefreshIntervalCatalog.Normalize(minutes, 12);
+ }
+
+ private void InitializeFrequencyOptions()
+ {
+ FrequencyComboBox.Items.Clear();
+ foreach (var minutes in SupportedIntervals)
+ {
+ FrequencyComboBox.Items.Add(new ComboBoxItem
+ {
+ Tag = minutes.ToString(),
+ Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
+ });
+ }
+ }
+
+ private void ApplyFrequencyLocalization()
+ {
+ foreach (var item in FrequencyComboBox.Items.OfType())
+ {
+ if (item.Tag is not string tagText ||
+ !int.TryParse(tagText, out var minutes))
+ {
+ continue;
+ }
+
+ var key = $"refresh.frequency.{RefreshIntervalCatalog.ToLocalizationKeySuffix(minutes)}";
+ item.Content = L(key, RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes));
+ }
+ }
+
+ private string L(string key, string fallback)
+ {
+ return _localizationService.GetString(_languageCode, key, fallback);
+ }
+}
diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
index d995aeb..fc322b8 100644
--- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
+++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs
@@ -389,6 +389,7 @@ public partial class MainWindow
CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection();
+ ClearSelectedLauncherTile(refreshTaskbar: false);
UpdateDesktopComponentHostEditState();
ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
@@ -425,6 +426,18 @@ public partial class MainWindow
if (context == TaskbarContext.Desktop && _isComponentLibraryOpen)
{
var actions = new List();
+ var isLauncherSurface = _currentDesktopSurfaceIndex == LauncherSurfaceIndex;
+ if (isLauncherSurface && IsLauncherTileSelected())
+ {
+ actions.Add(new TaskbarActionItem(
+ TaskbarActionId.HideLauncherEntry,
+ L("launcher.action.hide", "Hide"),
+ "Hide",
+ IsVisible: true,
+ CommandKey: "launcher.hide"));
+ return actions;
+ }
+
if (_selectedDesktopComponentHost is not null)
{
actions.Add(new TaskbarActionItem(
@@ -537,10 +550,11 @@ public partial class MainWindow
var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
action.Id == TaskbarActionId.DeleteComponent;
+ var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry;
var isEditAction = action.Id == TaskbarActionId.EditComponent;
Symbol iconSymbol;
- if (isDeleteAction)
+ if (isDeleteAction || isHideAction)
{
iconSymbol = Symbol.Delete;
}
@@ -582,7 +596,7 @@ public partial class MainWindow
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(padding),
- Foreground = isDeleteAction
+ Foreground = (isDeleteAction || isHideAction)
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground,
Tag = action.CommandKey
@@ -602,7 +616,7 @@ public partial class MainWindow
{
Text = action.Title,
FontSize = fontSize * 0.85,
- Foreground = isDeleteAction
+ Foreground = (isDeleteAction || isHideAction)
? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
@@ -646,6 +660,9 @@ public partial class MainWindow
case "component.edit":
OpenComponentSettings();
break;
+ case "launcher.hide":
+ HideSelectedLauncherEntry();
+ break;
}
}
@@ -719,6 +736,12 @@ public partial class MainWindow
return;
}
+ if (IsWeatherComponentId(placement.ComponentId))
+ {
+ OpenWeatherComponentSettings();
+ return;
+ }
+
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
{
OpenDailyArtworkComponentSettings();
@@ -743,6 +766,12 @@ public partial class MainWindow
return;
}
+ if (placement.ComponentId == BuiltInComponentIds.DesktopStcn24Forum)
+ {
+ OpenStcn24ForumComponentSettings();
+ return;
+ }
+
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{
OpenStudyEnvironmentComponentSettings();
@@ -750,6 +779,15 @@ public partial class MainWindow
}
}
+ private static bool IsWeatherComponentId(string componentId)
+ {
+ return string.Equals(componentId, BuiltInComponentIds.DesktopWeather, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(componentId, BuiltInComponentIds.DesktopWeatherClock, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(componentId, BuiltInComponentIds.DesktopHourlyWeather, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(componentId, BuiltInComponentIds.DesktopMultiDayWeather, StringComparison.OrdinalIgnoreCase) ||
+ string.Equals(componentId, BuiltInComponentIds.DesktopExtendedWeather, StringComparison.OrdinalIgnoreCase);
+ }
+
private void OpenDateComponentSettings()
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
@@ -814,6 +852,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1;
}
+ private void OpenWeatherComponentSettings()
+ {
+ if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
+ {
+ return;
+ }
+
+ var settingsContent = new WeatherWidgetSettingsWindow();
+ settingsContent.SettingsChanged += OnWeatherSettingsChanged;
+ ComponentSettingsContentHost.Content = settingsContent;
+
+ ComponentSettingsWindow.IsVisible = true;
+ ComponentSettingsWindow.Opacity = 0;
+ ComponentSettingsWindow.Opacity = 1;
+ }
+
private void OpenStudyEnvironmentComponentSettings()
{
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
@@ -894,6 +948,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1;
}
+ private void OpenStcn24ForumComponentSettings()
+ {
+ if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
+ {
+ return;
+ }
+
+ var settingsContent = new Stcn24ForumSettingsWindow();
+ settingsContent.SettingsChanged += OnStcn24ForumSettingsChanged;
+ ComponentSettingsContentHost.Content = settingsContent;
+
+ ComponentSettingsWindow.IsVisible = true;
+ ComponentSettingsWindow.Opacity = 0;
+ ComponentSettingsWindow.Opacity = 1;
+ }
+
private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
{
if (_selectedDesktopComponentHost is null)
@@ -988,6 +1058,43 @@ public partial class MainWindow
}
}
+ private void OnWeatherSettingsChanged(object? sender, EventArgs e)
+ {
+ _ = sender;
+ _ = e;
+
+ foreach (var pageGrid in _desktopPageComponentGrids.Values)
+ {
+ foreach (var host in pageGrid.Children.OfType())
+ {
+ if (!host.Classes.Contains(DesktopComponentHostClass))
+ {
+ continue;
+ }
+
+ var child = TryGetContentHost(host)?.Child;
+ switch (child)
+ {
+ case WeatherWidget weatherWidget:
+ weatherWidget.RefreshFromSettings();
+ break;
+ case WeatherClockWidget weatherClockWidget:
+ weatherClockWidget.RefreshFromSettings();
+ break;
+ case HourlyWeatherWidget hourlyWeatherWidget:
+ hourlyWeatherWidget.RefreshFromSettings();
+ break;
+ case MultiDayWeatherWidget multiDayWeatherWidget:
+ multiDayWeatherWidget.RefreshFromSettings();
+ break;
+ case ExtendedWeatherWidget extendedWeatherWidget:
+ extendedWeatherWidget.RefreshFromSettings();
+ break;
+ }
+ }
+ }
+ }
+
private void OnCnrDailyNewsSettingsChanged(object? sender, EventArgs e)
{
_ = sender;
@@ -1054,6 +1161,28 @@ public partial class MainWindow
}
}
+ private void OnStcn24ForumSettingsChanged(object? sender, EventArgs e)
+ {
+ _ = sender;
+ _ = e;
+
+ foreach (var pageGrid in _desktopPageComponentGrids.Values)
+ {
+ foreach (var host in pageGrid.Children.OfType())
+ {
+ if (!host.Classes.Contains(DesktopComponentHostClass))
+ {
+ continue;
+ }
+
+ if (TryGetContentHost(host)?.Child is Stcn24ForumWidget widget)
+ {
+ widget.RefreshFromSettings();
+ }
+ }
+ }
+ }
+
private void CloseComponentSettingsWindow()
{
if (ComponentSettingsWindow is null)
@@ -1086,6 +1215,11 @@ public partial class MainWindow
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
}
+ if (ComponentSettingsContentHost?.Content is WeatherWidgetSettingsWindow weatherSettingsWindow)
+ {
+ weatherSettingsWindow.SettingsChanged -= OnWeatherSettingsChanged;
+ }
+
if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow)
{
cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged;
@@ -1101,6 +1235,11 @@ public partial class MainWindow
bilibiliHotSearchSettingsWindow.SettingsChanged -= OnBilibiliHotSearchSettingsChanged;
}
+ if (ComponentSettingsContentHost?.Content is Stcn24ForumSettingsWindow stcn24ForumSettingsWindow)
+ {
+ stcn24ForumSettingsWindow.SettingsChanged -= OnStcn24ForumSettingsChanged;
+ }
+
ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() =>
@@ -1792,6 +1931,7 @@ public partial class MainWindow
CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection();
+ ClearSelectedLauncherTile(refreshTaskbar: false);
UpdateDesktopComponentHostEditState();
ClearComponentLibraryPreviewControls();
UpdateComponentLibraryLayout(_currentDesktopCellSize);
@@ -1901,6 +2041,8 @@ public partial class MainWindow
private void SetSelectedDesktopComponent(Border? host)
{
+ ClearSelectedLauncherTile(refreshTaskbar: false);
+
// Clear previous selection
if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host)
{
diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
index 2d37a1d..a823234 100644
--- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
+++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs
@@ -42,6 +42,9 @@ public partial class MainWindow
private readonly Stack _launcherFolderStack = [];
private readonly HashSet _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
+ private Button? _selectedLauncherTileButton;
+ private LauncherEntryKind? _selectedLauncherEntryKind;
+ private string? _selectedLauncherEntryKey;
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes;
private Bitmap? _launcherFolderIconBitmap;
@@ -341,6 +344,7 @@ public partial class MainWindow
if (_currentDesktopSurfaceIndex != LauncherSurfaceIndex)
{
CloseLauncherFolderOverlay();
+ ClearSelectedLauncherTile(refreshTaskbar: false);
}
UpdateDesktopPageAwareComponentContext();
@@ -386,12 +390,14 @@ public partial class MainWindow
return;
}
- // 如果在组件编辑模式下点击空白区域,取消组件选中
- if (_isComponentLibraryOpen && _selectedDesktopComponentHost is not null)
+ // 如果在组件编辑模式下点击空白区域,取消选中(组件或启动台图标)
+ if (_isComponentLibraryOpen &&
+ (_selectedDesktopComponentHost is not null || _selectedLauncherTileButton is not null))
{
if (!IsInteractivePointerSource(e.Source))
{
ClearDesktopComponentSelection();
+ ClearSelectedLauncherTile(refreshTaskbar: false);
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
}
@@ -737,6 +743,7 @@ public partial class MainWindow
return;
}
+ ClearSelectedLauncherTile(refreshTaskbar: false);
LauncherRootTilePanel.Children.Clear();
var folders = _startMenuRoot.Folders;
var apps = _startMenuRoot.Apps;
@@ -784,7 +791,8 @@ public partial class MainWindow
monogram: "DIR",
iconBitmap: folderIconBitmap,
() => OpenLauncherFolder(folder),
- hideAction: string.IsNullOrWhiteSpace(folderKey) ? null : () => HideLauncherFolder(folder));
+ LauncherEntryKind.Folder,
+ folderKey);
}
private Button CreateLauncherAppTile(StartMenuAppEntry app)
@@ -798,7 +806,8 @@ public partial class MainWindow
monogram,
iconBitmap,
() => LaunchStartMenuEntry(app),
- hideAction: string.IsNullOrWhiteSpace(appKey) ? null : () => HideLauncherApp(app));
+ LauncherEntryKind.Shortcut,
+ appKey);
}
private Control CreateLauncherHintTile(string title, string subtitle)
@@ -842,7 +851,8 @@ public partial class MainWindow
string monogram,
Bitmap? iconBitmap,
Action clickAction,
- Action? hideAction = null)
+ LauncherEntryKind entryKind,
+ string entryKey)
{
Control iconControl = iconBitmap is not null
? new Image
@@ -910,13 +920,26 @@ public partial class MainWindow
Classes = { "glass-panel" },
Margin = new Thickness(0, 0, 12, 12),
BorderThickness = new Thickness(0),
+ BorderBrush = Brushes.Transparent,
CornerRadius = new CornerRadius(20),
Padding = new Thickness(10),
Content = content
// 不设置固定 Width 和 Height,由 UpdateLauncherTileLayout 动态设置
};
- button.Click += (_, _) => clickAction();
- AttachLauncherTileContextMenu(button, hideAction);
+ button.Click += (_, _) =>
+ {
+ if (_isComponentLibraryOpen)
+ {
+ if (!string.IsNullOrWhiteSpace(entryKey))
+ {
+ SetSelectedLauncherTile(button, entryKind, entryKey);
+ }
+
+ return;
+ }
+
+ clickAction();
+ };
return button;
}
@@ -937,49 +960,100 @@ public partial class MainWindow
return string.IsNullOrWhiteSpace(key) || !_hiddenLauncherAppPaths.Contains(key);
}
- private void AttachLauncherTileContextMenu(Button tileButton, Action? hideAction)
+ private bool IsLauncherTileSelected()
{
- if (hideAction is null)
+ return _selectedLauncherEntryKind.HasValue && !string.IsNullOrWhiteSpace(_selectedLauncherEntryKey);
+ }
+
+ private void SetSelectedLauncherTile(Button button, LauncherEntryKind entryKind, string entryKey)
+ {
+ if (!_isComponentLibraryOpen || string.IsNullOrWhiteSpace(entryKey))
{
- tileButton.ContextMenu = null;
return;
}
- var hideItem = new MenuItem
+ var normalizedKey = NormalizeLauncherHiddenKey(entryKey);
+ if (string.IsNullOrWhiteSpace(normalizedKey))
{
- Header = L("launcher.context.hide_icon", "Hide Icon")
+ return;
+ }
+
+ if (_selectedDesktopComponentHost is not null)
+ {
+ ClearDesktopComponentSelection();
+ }
+
+ if (_selectedLauncherTileButton is not null && _selectedLauncherTileButton != button)
+ {
+ ApplyLauncherTileSelectionVisual(_selectedLauncherTileButton, isSelected: false);
+ }
+
+ _selectedLauncherTileButton = button;
+ _selectedLauncherEntryKind = entryKind;
+ _selectedLauncherEntryKey = normalizedKey;
+ ApplyLauncherTileSelectionVisual(button, isSelected: true);
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ }
+
+ private void ClearSelectedLauncherTile(bool refreshTaskbar)
+ {
+ if (_selectedLauncherTileButton is not null)
+ {
+ ApplyLauncherTileSelectionVisual(_selectedLauncherTileButton, isSelected: false);
+ }
+
+ _selectedLauncherTileButton = null;
+ _selectedLauncherEntryKind = null;
+ _selectedLauncherEntryKey = null;
+
+ if (refreshTaskbar)
+ {
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
+ }
+ }
+
+ private void ApplyLauncherTileSelectionVisual(Button button, bool isSelected)
+ {
+ var showSelection = isSelected && _isComponentLibraryOpen;
+ button.BorderThickness = showSelection
+ ? new Thickness(Math.Clamp(_currentDesktopCellSize * 0.04, 1, 3))
+ : new Thickness(0);
+ button.BorderBrush = showSelection ? GetThemeBrush("AdaptiveAccentBrush") : Brushes.Transparent;
+ }
+
+ private void HideSelectedLauncherEntry()
+ {
+ if (!_isComponentLibraryOpen ||
+ _currentDesktopSurfaceIndex != LauncherSurfaceIndex ||
+ _selectedLauncherEntryKind is null ||
+ string.IsNullOrWhiteSpace(_selectedLauncherEntryKey))
+ {
+ return;
+ }
+
+ var entryKind = _selectedLauncherEntryKind.Value;
+ var entryKey = _selectedLauncherEntryKey!;
+ ClearSelectedLauncherTile(refreshTaskbar: false);
+
+ var changed = entryKind switch
+ {
+ LauncherEntryKind.Folder => _hiddenLauncherFolderPaths.Add(entryKey),
+ LauncherEntryKind.Shortcut => _hiddenLauncherAppPaths.Add(entryKey),
+ _ => false
};
- hideItem.Click += (_, _) => hideAction();
- var contextMenu = new ContextMenu();
- contextMenu.Items.Add(hideItem);
- tileButton.ContextMenu = contextMenu;
- }
-
- private void HideLauncherFolder(StartMenuFolderNode folder)
- {
- var key = NormalizeLauncherHiddenKey(folder.RelativePath);
- if (string.IsNullOrWhiteSpace(key) || !_hiddenLauncherFolderPaths.Add(key))
+ if (changed)
{
+ ApplyLauncherVisibilitySettingsChange();
return;
}
- ApplyLauncherVisibilitySettingsChange();
- }
-
- private void HideLauncherApp(StartMenuAppEntry app)
- {
- var key = NormalizeLauncherHiddenKey(app.RelativePath);
- if (string.IsNullOrWhiteSpace(key) || !_hiddenLauncherAppPaths.Add(key))
- {
- return;
- }
-
- ApplyLauncherVisibilitySettingsChange();
+ ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void ApplyLauncherVisibilitySettingsChange()
{
+ ClearSelectedLauncherTile(refreshTaskbar: false);
RenderLauncherRootTiles();
if (_launcherFolderStack.Count > 0)
{
@@ -1278,6 +1352,7 @@ public partial class MainWindow
private void CloseLauncherFolderOverlay()
{
+ ClearSelectedLauncherTile(refreshTaskbar: false);
_launcherFolderStack.Clear();
if (LauncherFolderOverlay is not null)
{
@@ -1300,6 +1375,7 @@ public partial class MainWindow
return;
}
+ ClearSelectedLauncherTile(refreshTaskbar: false);
if (_launcherFolderStack.Count == 0)
{
CloseLauncherFolderOverlay();