From 72a0be16b3522aff84bc0d05d1841ba71c4d33bf Mon Sep 17 00:00:00 2001 From: lincube Date: Fri, 6 Mar 2026 10:32:02 +0800 Subject: [PATCH] 0.4.6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了托盘菜单,提供了应用启动台隐藏应用功能,优化了自动刷新功能,为STCN 24组件提供了更多信息选项。 --- LanMountainDesktop/App.axaml | 15 ++ LanMountainDesktop/App.axaml.cs | 52 +++++ LanMountainDesktop/Localization/en-US.json | 56 ++++- LanMountainDesktop/Localization/zh-CN.json | 56 ++++- .../Models/ComponentSettingsSnapshot.cs | 10 + .../Models/RefreshIntervalCatalog.cs | 74 +++++++ .../Models/Stcn24ForumSourceTypes.cs | 42 ++++ LanMountainDesktop/Models/TaskbarActionId.cs | 3 +- .../Services/ComponentSettingsService.cs | 71 +++---- .../Services/IRecommendationDataService.cs | 1 + .../Services/RecommendationDataService.cs | 125 +++++++++-- .../BilibiliHotSearchSettingsWindow.axaml.cs | 46 ++-- .../BilibiliHotSearchWidget.axaml.cs | 2 +- .../CnrDailyNewsSettingsWindow.axaml.cs | 46 ++-- .../Components/CnrDailyNewsWidget.axaml.cs | 2 +- .../DailyWordSettingsWindow.axaml.cs | 46 ++-- .../Views/Components/DailyWordWidget.axaml.cs | 3 +- .../Components/ExtendedWeatherWidget.axaml.cs | 62 +++++- .../Components/HourlyWeatherWidget.axaml.cs | 62 +++++- .../Components/MultiDayWeatherWidget.axaml.cs | 62 +++++- .../Stcn24ForumSettingsWindow.axaml | 128 +++++++++++ .../Stcn24ForumSettingsWindow.axaml.cs | 199 ++++++++++++++++++ .../Components/Stcn24ForumWidget.axaml.cs | 77 ++++++- .../Components/WeatherClockWidget.axaml.cs | 71 ++++++- .../Views/Components/WeatherWidget.axaml.cs | 62 +++++- .../WeatherWidgetSettingsWindow.axaml | 87 ++++++++ .../WeatherWidgetSettingsWindow.axaml.cs | 153 ++++++++++++++ .../Views/MainWindow.ComponentSystem.cs | 148 ++++++++++++- .../Views/MainWindow.DesktopPaging.cs | 144 ++++++++++--- 29 files changed, 1752 insertions(+), 153 deletions(-) create mode 100644 LanMountainDesktop/Models/RefreshIntervalCatalog.cs create mode 100644 LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs create mode 100644 LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml create mode 100644 LanMountainDesktop/Views/Components/Stcn24ForumSettingsWindow.axaml.cs create mode 100644 LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml create mode 100644 LanMountainDesktop/Views/Components/WeatherWidgetSettingsWindow.axaml.cs 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();