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

View File

@@ -16,6 +16,21 @@
<local:ViewLocator/> <local:ViewLocator/>
</Application.DataTemplates> </Application.DataTemplates>
<TrayIcon.Icons>
<TrayIcons>
<TrayIcon Icon="/Assets/avalonia-logo.ico"
ToolTipText="LanMountainDesktop">
<TrayIcon.Menu>
<NativeMenu>
<NativeMenuItem Header="重启应用" Click="OnTrayRestartClick" />
<NativeMenuItemSeparator />
<NativeMenuItem Header="退出应用" Click="OnTrayExitClick" />
</NativeMenu>
</TrayIcon.Menu>
</TrayIcon>
</TrayIcons>
</TrayIcon.Icons>
<Application.Styles> <Application.Styles>
<sty:FluentAvaloniaTheme /> <sty:FluentAvaloniaTheme />
<mi:MaterialIconStyles /> <mi:MaterialIconStyles />

View File

@@ -3,6 +3,7 @@ using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Data.Core.Plugins; using Avalonia.Data.Core.Plugins;
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
@@ -37,6 +38,57 @@ public partial class App : Application
base.OnFrameworkInitializationCompleted(); 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() private void DisableAvaloniaDataAnnotationValidation()
{ {
// Get an array of plugins to remove // Get an array of plugins to remove

View File

@@ -254,10 +254,11 @@
"launcher.empty_folder": "This folder is empty.", "launcher.empty_folder": "This folder is empty.",
"launcher.folder_items_format": "{0} apps", "launcher.folder_items_format": "{0} apps",
"launcher.context.hide_icon": "Hide Icon", "launcher.context.hide_icon": "Hide Icon",
"launcher.action.hide": "Hide",
"settings.launcher.title": "App Launcher", "settings.launcher.title": "App Launcher",
"settings.launcher.hidden_header": "Hidden Items", "settings.launcher.hidden_header": "Hidden Items",
"settings.launcher.hidden_desc": "Review hidden launcher entries and show them again.", "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_empty": "No hidden items.",
"settings.launcher.hidden_type_folder": "Folder", "settings.launcher.hidden_type_folder": "Folder",
"settings.launcher.hidden_type_shortcut": "Shortcut", "settings.launcher.hidden_type_shortcut": "Shortcut",
@@ -358,10 +359,63 @@
"bilihot.widget.fetch_failed": "Hot search fetch failed", "bilihot.widget.fetch_failed": "Hot search fetch failed",
"bilihot.widget.fallback_item": "No hot search data", "bilihot.widget.fallback_item": "No hot search data",
"bilihot.widget.more_hot": "More hot search", "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": "Loading...",
"stcn24.widget.loading_item": "Loading...", "stcn24.widget.loading_item": "Loading...",
"stcn24.widget.fetch_failed": "Forum posts fetch failed", "stcn24.widget.fetch_failed": "Forum posts fetch failed",
"stcn24.widget.fallback_item": "No posts", "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.loading": "Loading exchange rates...",
"exchange.widget.fetch_failed": "Exchange rate fetch failed", "exchange.widget.fetch_failed": "Exchange rate fetch failed",
"cnrnews.settings.title": "CNR Settings", "cnrnews.settings.title": "CNR Settings",

View File

@@ -254,10 +254,11 @@
"launcher.empty_folder": "此文件夹为空。", "launcher.empty_folder": "此文件夹为空。",
"launcher.folder_items_format": "{0} 个应用", "launcher.folder_items_format": "{0} 个应用",
"launcher.context.hide_icon": "隐藏图标", "launcher.context.hide_icon": "隐藏图标",
"launcher.action.hide": "隐藏",
"settings.launcher.title": "应用启动台", "settings.launcher.title": "应用启动台",
"settings.launcher.hidden_header": "已隐藏项目", "settings.launcher.hidden_header": "已隐藏项目",
"settings.launcher.hidden_desc": "查看已隐藏的启动台项目并重新显示。", "settings.launcher.hidden_desc": "查看已隐藏的启动台项目并重新显示。",
"settings.launcher.hidden_hint": "在启动台中右键图标可隐藏,隐藏后的项目会显示在这里。", "settings.launcher.hidden_hint": "进入桌面编辑模式后,在启动台选中图标并点击“隐藏,隐藏后的项目会显示在这里。",
"settings.launcher.hidden_empty": "暂无隐藏项目。", "settings.launcher.hidden_empty": "暂无隐藏项目。",
"settings.launcher.hidden_type_folder": "文件夹", "settings.launcher.hidden_type_folder": "文件夹",
"settings.launcher.hidden_type_shortcut": "快捷方式", "settings.launcher.hidden_type_shortcut": "快捷方式",
@@ -358,10 +359,63 @@
"bilihot.widget.fetch_failed": "热搜获取失败", "bilihot.widget.fetch_failed": "热搜获取失败",
"bilihot.widget.fallback_item": "暂无热搜", "bilihot.widget.fallback_item": "暂无热搜",
"bilihot.widget.more_hot": "更多热搜", "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": "加载中...",
"stcn24.widget.loading_item": "加载中...", "stcn24.widget.loading_item": "加载中...",
"stcn24.widget.fetch_failed": "帖子获取失败", "stcn24.widget.fetch_failed": "帖子获取失败",
"stcn24.widget.fallback_item": "暂无帖子", "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.loading": "正在加载汇率...",
"exchange.widget.fetch_failed": "汇率获取失败", "exchange.widget.fetch_failed": "汇率获取失败",
"cnrnews.settings.title": "央广网设置", "cnrnews.settings.title": "央广网设置",

View File

@@ -40,6 +40,16 @@ public sealed class ComponentSettingsSnapshot
public int BilibiliHotSearchAutoRefreshIntervalMinutes { get; set; } = 15; 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() public ComponentSettingsSnapshot Clone()
{ {
var clone = (ComponentSettingsSnapshot)MemberwiseClone(); var clone = (ComponentSettingsSnapshot)MemberwiseClone();

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace LanMountainDesktop.Models;
public static class RefreshIntervalCatalog
{
public static IReadOnlyList<int> 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"
};
}
}

View File

@@ -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<string> 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;
}
}

View File

@@ -7,5 +7,6 @@ public enum TaskbarActionId
AddDesktopPage, AddDesktopPage,
DeleteDesktopPage, DeleteDesktopPage,
DeleteComponent, DeleteComponent,
EditComponent EditComponent,
HideLauncherEntry
} }

View File

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

View File

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

View File

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

View File

@@ -1,15 +1,17 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class BilibiliHotSearchSettingsWindow : UserControl public partial class BilibiliHotSearchSettingsWindow : UserControl
{ {
private static readonly int[] SupportedIntervals = [5, 10, 15, 30, 60, 180]; private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new(); private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
public BilibiliHotSearchSettingsWindow() public BilibiliHotSearchSettingsWindow()
{ {
InitializeComponent(); InitializeComponent();
InitializeFrequencyOptions();
LoadState(); LoadState();
ApplyLocalization(); ApplyLocalization();
} }
@@ -49,12 +52,7 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
AutoRefreshLabelTextBlock.Text = L("bilihot.settings.auto_refresh_label", "Auto refresh"); AutoRefreshLabelTextBlock.Text = L("bilihot.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("bilihot.settings.auto_refresh_enabled", "Enable auto refresh"); AutoRefreshCheckBox.Content = L("bilihot.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("bilihot.settings.frequency_label", "Refresh interval"); FrequencyLabelTextBlock.Text = L("bilihot.settings.frequency_label", "Refresh interval");
Frequency5mItem.Content = L("bilihot.settings.frequency_5m", "5 min"); ApplyFrequencyLocalization();
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");
} }
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class BilibiliHotSearchSettingsWindow : UserControl
private static int NormalizeInterval(int minutes) private static int NormalizeInterval(int minutes)
{ {
if (minutes <= 0) return RefreshIntervalCatalog.Normalize(minutes, 15);
{
return 15;
} }
if (SupportedIntervals.Contains(minutes)) private void InitializeFrequencyOptions()
{ {
return minutes; FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
} }
return SupportedIntervals private void ApplyFrequencyLocalization()
.OrderBy(value => Math.Abs(value - minutes)) {
.FirstOrDefault(15); foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
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) private string L(string key, string fallback)

View File

@@ -25,7 +25,7 @@ public partial class BilibiliHotSearchWidget : UserControl, IDesktopComponentWid
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2; private const int BaseHeightCells = 2;
private const int MaxDisplayItemCount = 4; private const int MaxDisplayItemCount = 4;
private static readonly int[] SupportedAutoRefreshIntervalsMinutes = [5, 10, 15, 30, 60, 180]; private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {

View File

@@ -1,15 +1,17 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class CnrDailyNewsSettingsWindow : UserControl public partial class CnrDailyNewsSettingsWindow : UserControl
{ {
private static readonly int[] SupportedIntervals = [5, 10, 40, 60, 720, 1440]; private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new(); private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
public CnrDailyNewsSettingsWindow() public CnrDailyNewsSettingsWindow()
{ {
InitializeComponent(); InitializeComponent();
InitializeFrequencyOptions();
LoadState(); LoadState();
ApplyLocalization(); ApplyLocalization();
} }
@@ -49,12 +52,7 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation"); AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation");
AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation"); AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation");
FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval"); FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval");
Frequency5mItem.Content = L("cnrnews.settings.frequency_5m", "5 min"); ApplyFrequencyLocalization();
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");
} }
private void OnAutoRotateChanged(object? sender, RoutedEventArgs e) private void OnAutoRotateChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class CnrDailyNewsSettingsWindow : UserControl
private static int NormalizeInterval(int minutes) private static int NormalizeInterval(int minutes)
{ {
if (minutes <= 0) return RefreshIntervalCatalog.Normalize(minutes, 60);
{
return 60;
} }
if (SupportedIntervals.Contains(minutes)) private void InitializeFrequencyOptions()
{ {
return minutes; FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
} }
return SupportedIntervals private void ApplyFrequencyLocalization()
.OrderBy(value => Math.Abs(value - minutes)) {
.FirstOrDefault(60); foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
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) private string L(string key, string fallback)

View File

@@ -36,7 +36,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private const double BaseCellSize = 48d; private const double BaseCellSize = 48d;
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2; private const int BaseHeightCells = 2;
private static readonly int[] SupportedAutoRotateIntervalsMinutes = [5, 10, 40, 60, 720, 1440]; private static readonly IReadOnlyList<int> SupportedAutoRotateIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {

View File

@@ -1,15 +1,17 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using LanMountainDesktop.Models;
using LanMountainDesktop.Services; using LanMountainDesktop.Services;
namespace LanMountainDesktop.Views.Components; namespace LanMountainDesktop.Views.Components;
public partial class DailyWordSettingsWindow : UserControl public partial class DailyWordSettingsWindow : UserControl
{ {
private static readonly int[] SupportedIntervals = [30, 60, 180, 360, 720, 1440]; private static readonly IReadOnlyList<int> SupportedIntervals = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new(); private readonly ComponentSettingsService _componentSettingsService = new();
@@ -22,6 +24,7 @@ public partial class DailyWordSettingsWindow : UserControl
public DailyWordSettingsWindow() public DailyWordSettingsWindow()
{ {
InitializeComponent(); InitializeComponent();
InitializeFrequencyOptions();
LoadState(); LoadState();
ApplyLocalization(); ApplyLocalization();
} }
@@ -49,12 +52,7 @@ public partial class DailyWordSettingsWindow : UserControl
AutoRefreshLabelTextBlock.Text = L("dailyword.settings.auto_refresh_label", "Auto refresh"); AutoRefreshLabelTextBlock.Text = L("dailyword.settings.auto_refresh_label", "Auto refresh");
AutoRefreshCheckBox.Content = L("dailyword.settings.auto_refresh_enabled", "Enable auto refresh"); AutoRefreshCheckBox.Content = L("dailyword.settings.auto_refresh_enabled", "Enable auto refresh");
FrequencyLabelTextBlock.Text = L("dailyword.settings.frequency_label", "Refresh interval"); FrequencyLabelTextBlock.Text = L("dailyword.settings.frequency_label", "Refresh interval");
Frequency30mItem.Content = L("dailyword.settings.frequency_30m", "30 min"); ApplyFrequencyLocalization();
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");
} }
private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e) private void OnAutoRefreshChanged(object? sender, RoutedEventArgs e)
@@ -117,19 +115,35 @@ public partial class DailyWordSettingsWindow : UserControl
private static int NormalizeInterval(int minutes) private static int NormalizeInterval(int minutes)
{ {
if (minutes <= 0) return RefreshIntervalCatalog.Normalize(minutes, 360);
{
return 360;
} }
if (SupportedIntervals.Contains(minutes)) private void InitializeFrequencyOptions()
{ {
return minutes; FrequencyComboBox.Items.Clear();
foreach (var minutes in SupportedIntervals)
{
FrequencyComboBox.Items.Add(new ComboBoxItem
{
Tag = minutes.ToString(),
Content = RefreshIntervalCatalog.ToEnglishFallbackLabel(minutes)
});
}
} }
return SupportedIntervals private void ApplyFrequencyLocalization()
.OrderBy(value => Math.Abs(value - minutes)) {
.FirstOrDefault(360); foreach (var item in FrequencyComboBox.Items.OfType<ComboBoxItem>())
{
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) private string L(string key, string fallback)

View File

@@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
@@ -22,7 +23,7 @@ public partial class DailyWordWidget : UserControl, IDesktopComponentWidget, IRe
private const double BaseCellSize = 48d; private const double BaseCellSize = 48d;
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 2; private const int BaseHeightCells = 2;
private static readonly int[] SupportedAutoRefreshIntervalsMinutes = [30, 60, 180, 360, 720, 1440]; private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {

View File

@@ -18,12 +18,14 @@ namespace LanMountainDesktop.Views.Components;
public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidget, IDesktopPageVisibilityAwareComponentWidget, ITimeZoneAwareComponentWidget, IWeatherInfoAwareComponentWidget
{ {
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) }; private readonly DispatcherTimer _refreshTimer = new() { Interval = TimeSpan.FromMinutes(12) };
private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval }; private readonly DispatcherTimer _animationTimer = new() { Interval = FluttermotionToken.WeatherAnimationFrameInterval };
private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1); private readonly ScaleTransform _backgroundMotionScaleTransform = new(1, 1);
private readonly TranslateTransform _backgroundMotionTranslateTransform = new(); private readonly TranslateTransform _backgroundMotionTranslateTransform = new();
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService; private IWeatherInfoService _weatherInfoService = DefaultWeatherInfoService;
@@ -34,6 +36,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isAttached; private bool _isAttached;
private bool _isOnActivePage = true; private bool _isOnActivePage = true;
private bool _isRefreshing; private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private string _languageCode = "zh-CN"; private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.ClearDay; private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.ClearDay;
private readonly TextBlock[] _hourlyTempBlocks; private readonly TextBlock[] _hourlyTempBlocks;
@@ -87,6 +90,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyVisualTheme(_activeVisualKind); ApplyVisualTheme(_activeVisualKind);
ApplyFallback(); ApplyFallback();
ApplyAutoRefreshSettings();
} }
private void ConfigureTextOverflowGuards() 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) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;
@@ -184,6 +197,7 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
ApplyAutoRefreshSettings();
UpdateTimerState(); UpdateTimerState();
if (_isOnActivePage) if (_isOnActivePage)
{ {
@@ -893,10 +907,14 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
{ {
if (_isAttached && _isOnActivePage) if (_isAttached && _isOnActivePage)
{ {
if (!_refreshTimer.IsEnabled) if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{ {
_refreshTimer.Start(); _refreshTimer.Start();
} }
else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
{
_refreshTimer.Stop();
}
if (!_animationTimer.IsEnabled) if (!_animationTimer.IsEnabled)
{ {
@@ -910,6 +928,48 @@ public partial class ExtendedWeatherWidget : UserControl, IDesktopComponentWidge
_animationTimer.Stop(); _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() private void CancelRefresh()
{ {
var cts = Interlocked.Exchange(ref _refreshCts, null); var cts = Interlocked.Exchange(ref _refreshCts, null);

View File

@@ -82,6 +82,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
string TemperatureText); string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {
@@ -94,6 +95,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new(); private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new(); private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -115,6 +117,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private bool _isAttached; private bool _isAttached;
private bool _isOnActivePage = true; private bool _isOnActivePage = true;
private bool _isRefreshing; private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private readonly TextBlock[] _hourlyTimeBlocks; private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks; private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks; private readonly TextBlock[] _hourlyTempBlocks;
@@ -147,6 +150,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
ApplyVisualTheme(WeatherVisualKind.ClearDay); ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState(); ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyAutoRefreshSettings();
} }
private void ConfigureTextOverflowGuards() 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) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;
@@ -249,6 +262,7 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
ApplyAutoRefreshSettings();
UpdateTimerState(); UpdateTimerState();
if (_isOnActivePage) if (_isOnActivePage)
{ {
@@ -1382,10 +1396,14 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
{ {
if (_isAttached && _isOnActivePage) if (_isAttached && _isOnActivePage)
{ {
if (!_refreshTimer.IsEnabled) if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{ {
_refreshTimer.Start(); _refreshTimer.Start();
} }
else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
{
_refreshTimer.Stop();
}
if (!_backgroundAnimationTimer.IsEnabled) if (!_backgroundAnimationTimer.IsEnabled)
{ {
@@ -1399,6 +1417,48 @@ public partial class HourlyWeatherWidget : UserControl, IDesktopComponentWidget,
_backgroundAnimationTimer.Stop(); _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() private void InitializeParticleVisuals()
{ {
if (_particleVisuals.Count > 0) if (_particleVisuals.Count > 0)

View File

@@ -80,6 +80,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
string TemperatureText); string TemperatureText);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {
@@ -92,6 +93,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new(); private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new(); private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -113,6 +115,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private bool _isAttached; private bool _isAttached;
private bool _isOnActivePage = true; private bool _isOnActivePage = true;
private bool _isRefreshing; private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private readonly TextBlock[] _hourlyTimeBlocks; private readonly TextBlock[] _hourlyTimeBlocks;
private readonly Image[] _hourlyIconBlocks; private readonly Image[] _hourlyIconBlocks;
private readonly TextBlock[] _hourlyTempBlocks; private readonly TextBlock[] _hourlyTempBlocks;
@@ -145,6 +148,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
ApplyVisualTheme(WeatherVisualKind.ClearDay); ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState(); ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyAutoRefreshSettings();
} }
private void ConfigureTextOverflowGuards() 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) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;
@@ -247,6 +260,7 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
ApplyAutoRefreshSettings();
UpdateTimerState(); UpdateTimerState();
if (_isOnActivePage) if (_isOnActivePage)
{ {
@@ -1232,10 +1246,14 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
{ {
if (_isAttached && _isOnActivePage) if (_isAttached && _isOnActivePage)
{ {
if (!_refreshTimer.IsEnabled) if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{ {
_refreshTimer.Start(); _refreshTimer.Start();
} }
else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
{
_refreshTimer.Stop();
}
if (!_backgroundAnimationTimer.IsEnabled) if (!_backgroundAnimationTimer.IsEnabled)
{ {
@@ -1249,6 +1267,48 @@ public partial class MultiDayWeatherWidget : UserControl, IDesktopComponentWidge
_backgroundAnimationTimer.Stop(); _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() private void InitializeParticleVisuals()
{ {
if (_particleVisuals.Count > 0) if (_particleVisuals.Count > 0)

View File

@@ -0,0 +1,128 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.Stcn24ForumSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="STCN 24 settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure information source, auto refresh and refresh interval."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="SourceLabelTextBlock"
Text="Information source"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="SourceComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnSourceSelectionChanged">
<ComboBoxItem x:Name="SourceLatestCreatedItem"
Tag="LatestCreated"
Content="Latest posts" />
<ComboBoxItem x:Name="SourceLatestActivityItem"
Tag="LatestActivity"
Content="Latest activity" />
<ComboBoxItem x:Name="SourceMostRepliesItem"
Tag="MostReplies"
Content="Most replies" />
<ComboBoxItem x:Name="SourceEarliestCreatedItem"
Tag="EarliestCreated"
Content="Earliest posts" />
<ComboBoxItem x:Name="SourceEarliestActivityItem"
Tag="EarliestActivity"
Content="Earliest activity" />
<ComboBoxItem x:Name="SourceLeastRepliesItem"
Tag="LeastReplies"
Content="Least replies" />
<ComboBoxItem x:Name="SourceFrontpageLatestItem"
Tag="FrontpageLatest"
Content="Frontpage latest" />
<ComboBoxItem x:Name="SourceFrontpageEarliestItem"
Tag="FrontpageEarliest"
Content="Frontpage earliest" />
</ComboBox>
</StackPanel>
</Border>
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency5mItem"
Tag="5"
Content="5 min" />
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency20mItem"
Tag="20"
Content="20 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -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<int> 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<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().FirstOrDefault();
}
private void SelectSourceType(string sourceType)
{
var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
var selected = SourceComboBox.Items
.OfType<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string sourceTag &&
string.Equals(Stcn24ForumSourceTypes.Normalize(sourceTag), normalizedSourceType, StringComparison.OrdinalIgnoreCase));
SourceComboBox.SelectedItem = selected ?? SourceComboBox.Items.OfType<ComboBoxItem>().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<ComboBoxItem>())
{
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);
}
}

View File

@@ -36,6 +36,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 4; private const int BaseHeightCells = 4;
private const int MaxDisplayItemCount = 4; private const int MaxDisplayItemCount = 4;
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {
@@ -43,6 +44,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
}; };
private readonly AppSettingsService _appSettingsService = new(); private readonly AppSettingsService _appSettingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly List<Stcn24ForumPostItemSnapshot> _activeItems = []; private readonly List<Stcn24ForumPostItemSnapshot> _activeItems = [];
private readonly List<ForumItemVisual> _itemVisuals = []; private readonly List<ForumItemVisual> _itemVisuals = [];
@@ -51,9 +53,11 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
private IRecommendationInfoService _recommendationService = DefaultRecommendationService; private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
private CancellationTokenSource? _refreshCts; private CancellationTokenSource? _refreshCts;
private string _languageCode = "zh-CN"; private string _languageCode = "zh-CN";
private string _sourceType = Stcn24ForumSourceTypes.LatestCreated;
private double _currentCellSize = BaseCellSize; private double _currentCellSize = BaseCellSize;
private bool _isAttached; private bool _isAttached;
private bool _isRefreshing; private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
private sealed record ForumItemVisual( private sealed record ForumItemVisual(
Border Host, Border Host,
@@ -114,6 +118,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
UpdateLanguageCode(); UpdateLanguageCode();
ApplyAutoRefreshSettings();
ApplyLoadingState(); ApplyLoadingState();
UpdateInteractionState(); UpdateInteractionState();
UpdateRefreshButtonState(); 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) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
if (!_refreshTimer.IsEnabled) ApplyAutoRefreshSettings();
{
_refreshTimer.Start();
}
_ = RefreshPostsAsync(forceRefresh: false); _ = RefreshPostsAsync(forceRefresh: false);
} }
@@ -211,6 +223,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
var query = new Stcn24ForumPostsQuery( var query = new Stcn24ForumPostsQuery(
Locale: _languageCode, Locale: _languageCode,
ItemCount: MaxDisplayItemCount, ItemCount: MaxDisplayItemCount,
SourceType: _sourceType,
ForceRefresh: forceRefresh); ForceRefresh: forceRefresh);
var result = await _recommendationService.GetStcn24ForumPostsAsync(query, cts.Token); var result = await _recommendationService.GetStcn24ForumPostsAsync(query, cts.Token);
if (!_isAttached || cts.IsCancellationRequested) 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() private void UpdateAdaptiveLayout()
{ {
var scale = ResolveScale(); var scale = ResolveScale();

View File

@@ -1,5 +1,7 @@
using System; using System;
using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
@@ -26,6 +28,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private const double DialCenter = DialDesignSize / 2d; private const double DialCenter = DialDesignSize / 2d;
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _clockTimer = new() private readonly DispatcherTimer _clockTimer = new()
{ {
@@ -38,6 +41,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0); private readonly Line _hourHandLine = CreateHandLine("#232938", 4.0);
private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8); private readonly Line _minuteHandLine = CreateHandLine("#2F3749", 2.8);
@@ -51,6 +55,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private bool _dialInitialized; private bool _dialInitialized;
private bool _handsInitialized; private bool _handsInitialized;
private bool _isRefreshing; private bool _isRefreshing;
private bool _weatherAutoRefreshEnabled = true;
private bool? _isNightModeApplied; private bool? _isNightModeApplied;
private string _languageCode = "zh-CN"; private string _languageCode = "zh-CN";
private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay; private HyperOS3WeatherVisualKind _activeVisualKind = HyperOS3WeatherVisualKind.CloudyDay;
@@ -70,6 +75,7 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyDefaultWeatherIcon(); ApplyDefaultWeatherIcon();
UpdateClockVisual(); UpdateClockVisual();
ApplyAutoRefreshSettings();
} }
public void SetTimeZoneService(TimeZoneService timeZoneService) 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) public void ApplyCellSize(double cellSize)
{ {
_currentCellSize = Math.Max(1, cellSize); _currentCellSize = Math.Max(1, cellSize);
@@ -203,9 +218,10 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
ApplyAutoRefreshSettings();
UpdateClockVisual(); UpdateClockVisual();
_clockTimer.Start(); _clockTimer.Start();
_weatherRefreshTimer.Start(); UpdateWeatherRefreshTimerState();
_ = RefreshWeatherAsync(forceRefresh: false); _ = RefreshWeatherAsync(forceRefresh: false);
} }
@@ -629,6 +645,59 @@ public partial class WeatherClockWidget : UserControl, IDesktopComponentWidget,
return Math.Clamp(value, -180, 180); 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() private void CancelRefreshRequest()
{ {
var cts = Interlocked.Exchange(ref _refreshCts, null); var cts = Interlocked.Exchange(ref _refreshCts, null);

View File

@@ -76,6 +76,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
double Longitude); double Longitude);
private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService(); private static readonly IWeatherInfoService DefaultWeatherInfoService = new XiaomiWeatherService();
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
{ {
@@ -88,6 +89,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
}; };
private readonly AppSettingsService _settingsService = new(); private readonly AppSettingsService _settingsService = new();
private readonly ComponentSettingsService _componentSettingsService = new();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new(); private readonly Dictionary<WeatherVisualKind, IBrush> _backgroundBrushCache = new();
private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new(); private readonly Dictionary<HyperOS3WeatherVisualKind, IBrush> _particleBrushCache = new();
@@ -109,6 +111,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private bool _isAttached; private bool _isAttached;
private bool _isOnActivePage = true; private bool _isOnActivePage = true;
private bool _isRefreshing; private bool _isRefreshing;
private bool _autoRefreshEnabled = true;
public WeatherWidget() public WeatherWidget()
{ {
@@ -125,6 +128,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
ApplyVisualTheme(WeatherVisualKind.ClearDay); ApplyVisualTheme(WeatherVisualKind.ClearDay);
ApplyNotConfiguredState(); ApplyNotConfiguredState();
ApplyCellSize(_currentCellSize); ApplyCellSize(_currentCellSize);
ApplyAutoRefreshSettings();
} }
public void SetTimeZoneService(TimeZoneService timeZoneService) 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) public void SetDesktopPageContext(bool isOnActivePage, bool isEditMode)
{ {
_ = isEditMode; _ = isEditMode;
@@ -194,6 +207,7 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
_isAttached = true; _isAttached = true;
ApplyAutoRefreshSettings();
UpdateTimerState(); UpdateTimerState();
if (_isOnActivePage) if (_isOnActivePage)
{ {
@@ -1021,10 +1035,14 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
{ {
if (_isAttached && _isOnActivePage) if (_isAttached && _isOnActivePage)
{ {
if (!_refreshTimer.IsEnabled) if (_autoRefreshEnabled && !_refreshTimer.IsEnabled)
{ {
_refreshTimer.Start(); _refreshTimer.Start();
} }
else if (!_autoRefreshEnabled && _refreshTimer.IsEnabled)
{
_refreshTimer.Stop();
}
if (!_backgroundAnimationTimer.IsEnabled) if (!_backgroundAnimationTimer.IsEnabled)
{ {
@@ -1038,6 +1056,48 @@ public partial class WeatherWidget : UserControl, IDesktopComponentWidget, IDesk
_backgroundAnimationTimer.Stop(); _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() private void InitializeParticleVisuals()
{ {
if (_particleVisuals.Count > 0) if (_particleVisuals.Count > 0)

View File

@@ -0,0 +1,87 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
d:DesignWidth="420"
d:DesignHeight="300"
x:Class="LanMountainDesktop.Views.Components.WeatherWidgetSettingsWindow">
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
Padding="16">
<Grid RowDefinitions="Auto,Auto,*"
RowSpacing="10">
<TextBlock x:Name="TitleTextBlock"
Text="Weather widget settings"
FontSize="18"
FontWeight="SemiBold"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<TextBlock x:Name="DescriptionTextBlock"
Grid.Row="1"
Text="Configure auto refresh and refresh interval for all weather widgets."
FontSize="12"
TextWrapping="Wrap"
Foreground="{DynamicResource AdaptiveTextSecondaryBrush}" />
<ScrollViewer Grid.Row="2"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto">
<StackPanel Spacing="10"
Margin="0,0,6,0">
<Border Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12">
<StackPanel Spacing="6">
<TextBlock x:Name="AutoRefreshLabelTextBlock"
Text="Auto refresh"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<CheckBox x:Name="AutoRefreshCheckBox"
Content="Enable auto refresh"
Checked="OnAutoRefreshChanged"
Unchecked="OnAutoRefreshChanged" />
</StackPanel>
</Border>
<Border x:Name="FrequencyCardBorder"
Background="{DynamicResource AdaptiveSurfaceRaisedBrush}"
BorderBrush="{DynamicResource AdaptiveButtonBorderBrush}"
BorderThickness="1"
CornerRadius="12"
Padding="12"
IsVisible="False">
<StackPanel Spacing="6">
<TextBlock x:Name="FrequencyLabelTextBlock"
Text="Refresh interval"
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
<ComboBox x:Name="FrequencyComboBox"
HorizontalAlignment="Stretch"
MinWidth="0"
SelectionChanged="OnFrequencySelectionChanged">
<ComboBoxItem x:Name="Frequency10mItem"
Tag="10"
Content="10 min" />
<ComboBoxItem x:Name="Frequency12mItem"
Tag="12"
Content="12 min" />
<ComboBoxItem x:Name="Frequency15mItem"
Tag="15"
Content="15 min" />
<ComboBoxItem x:Name="Frequency30mItem"
Tag="30"
Content="30 min" />
<ComboBoxItem x:Name="Frequency1hItem"
Tag="60"
Content="1 hour" />
<ComboBoxItem x:Name="Frequency3hItem"
Tag="180"
Content="3 hours" />
</ComboBox>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@@ -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<int> 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<ComboBoxItem>()
.FirstOrDefault(item =>
item.Tag is string tagText &&
int.TryParse(tagText, out var minutes) &&
minutes == intervalMinutes);
FrequencyComboBox.SelectedItem = selected ?? FrequencyComboBox.Items.OfType<ComboBoxItem>().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<ComboBoxItem>())
{
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);
}
}

View File

@@ -389,6 +389,7 @@ public partial class MainWindow
CancelDesktopComponentDrag(); CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true); CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection(); ClearDesktopComponentSelection();
ClearSelectedLauncherTile(refreshTaskbar: false);
UpdateDesktopComponentHostEditState(); UpdateDesktopComponentHostEditState();
ComponentLibraryWindow.Opacity = 0; ComponentLibraryWindow.Opacity = 0;
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
@@ -425,6 +426,18 @@ public partial class MainWindow
if (context == TaskbarContext.Desktop && _isComponentLibraryOpen) if (context == TaskbarContext.Desktop && _isComponentLibraryOpen)
{ {
var actions = new List<TaskbarActionItem>(); var actions = new List<TaskbarActionItem>();
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) if (_selectedDesktopComponentHost is not null)
{ {
actions.Add(new TaskbarActionItem( actions.Add(new TaskbarActionItem(
@@ -537,10 +550,11 @@ public partial class MainWindow
var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage || var isDeleteAction = action.Id == TaskbarActionId.DeleteDesktopPage ||
action.Id == TaskbarActionId.DeleteComponent; action.Id == TaskbarActionId.DeleteComponent;
var isHideAction = action.Id == TaskbarActionId.HideLauncherEntry;
var isEditAction = action.Id == TaskbarActionId.EditComponent; var isEditAction = action.Id == TaskbarActionId.EditComponent;
Symbol iconSymbol; Symbol iconSymbol;
if (isDeleteAction) if (isDeleteAction || isHideAction)
{ {
iconSymbol = Symbol.Delete; iconSymbol = Symbol.Delete;
} }
@@ -582,7 +596,7 @@ public partial class MainWindow
Background = Brushes.Transparent, Background = Brushes.Transparent,
BorderThickness = new Thickness(0), BorderThickness = new Thickness(0),
Padding = new Thickness(padding), Padding = new Thickness(padding),
Foreground = isDeleteAction Foreground = (isDeleteAction || isHideAction)
? new SolidColorBrush(Color.Parse("#FFFF6B6B")) ? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground, : Foreground,
Tag = action.CommandKey Tag = action.CommandKey
@@ -602,7 +616,7 @@ public partial class MainWindow
{ {
Text = action.Title, Text = action.Title,
FontSize = fontSize * 0.85, FontSize = fontSize * 0.85,
Foreground = isDeleteAction Foreground = (isDeleteAction || isHideAction)
? new SolidColorBrush(Color.Parse("#FFFF6B6B")) ? new SolidColorBrush(Color.Parse("#FFFF6B6B"))
: Foreground, : Foreground,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
@@ -646,6 +660,9 @@ public partial class MainWindow
case "component.edit": case "component.edit":
OpenComponentSettings(); OpenComponentSettings();
break; break;
case "launcher.hide":
HideSelectedLauncherEntry();
break;
} }
} }
@@ -719,6 +736,12 @@ public partial class MainWindow
return; return;
} }
if (IsWeatherComponentId(placement.ComponentId))
{
OpenWeatherComponentSettings();
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork) if (placement.ComponentId == BuiltInComponentIds.DesktopDailyArtwork)
{ {
OpenDailyArtworkComponentSettings(); OpenDailyArtworkComponentSettings();
@@ -743,6 +766,12 @@ public partial class MainWindow
return; return;
} }
if (placement.ComponentId == BuiltInComponentIds.DesktopStcn24Forum)
{
OpenStcn24ForumComponentSettings();
return;
}
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment) if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
{ {
OpenStudyEnvironmentComponentSettings(); 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() private void OpenDateComponentSettings()
{ {
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null) if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
@@ -814,6 +852,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1; 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() private void OpenStudyEnvironmentComponentSettings()
{ {
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null) if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
@@ -894,6 +948,22 @@ public partial class MainWindow
ComponentSettingsWindow.Opacity = 1; 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) private void OnClassScheduleSettingsChanged(object? sender, EventArgs e)
{ {
if (_selectedDesktopComponentHost is null) 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<Border>())
{
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) private void OnCnrDailyNewsSettingsChanged(object? sender, EventArgs e)
{ {
_ = sender; _ = 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<Border>())
{
if (!host.Classes.Contains(DesktopComponentHostClass))
{
continue;
}
if (TryGetContentHost(host)?.Child is Stcn24ForumWidget widget)
{
widget.RefreshFromSettings();
}
}
}
}
private void CloseComponentSettingsWindow() private void CloseComponentSettingsWindow()
{ {
if (ComponentSettingsWindow is null) if (ComponentSettingsWindow is null)
@@ -1086,6 +1215,11 @@ public partial class MainWindow
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged; worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
} }
if (ComponentSettingsContentHost?.Content is WeatherWidgetSettingsWindow weatherSettingsWindow)
{
weatherSettingsWindow.SettingsChanged -= OnWeatherSettingsChanged;
}
if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow) if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow)
{ {
cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged; cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged;
@@ -1101,6 +1235,11 @@ public partial class MainWindow
bilibiliHotSearchSettingsWindow.SettingsChanged -= OnBilibiliHotSearchSettingsChanged; bilibiliHotSearchSettingsWindow.SettingsChanged -= OnBilibiliHotSearchSettingsChanged;
} }
if (ComponentSettingsContentHost?.Content is Stcn24ForumSettingsWindow stcn24ForumSettingsWindow)
{
stcn24ForumSettingsWindow.SettingsChanged -= OnStcn24ForumSettingsChanged;
}
ComponentSettingsWindow.Opacity = 0; ComponentSettingsWindow.Opacity = 0;
DispatcherTimer.RunOnce(() => DispatcherTimer.RunOnce(() =>
@@ -1792,6 +1931,7 @@ public partial class MainWindow
CancelDesktopComponentDrag(); CancelDesktopComponentDrag();
CancelDesktopComponentResize(restoreOriginalSpan: true); CancelDesktopComponentResize(restoreOriginalSpan: true);
ClearDesktopComponentSelection(); ClearDesktopComponentSelection();
ClearSelectedLauncherTile(refreshTaskbar: false);
UpdateDesktopComponentHostEditState(); UpdateDesktopComponentHostEditState();
ClearComponentLibraryPreviewControls(); ClearComponentLibraryPreviewControls();
UpdateComponentLibraryLayout(_currentDesktopCellSize); UpdateComponentLibraryLayout(_currentDesktopCellSize);
@@ -1901,6 +2041,8 @@ public partial class MainWindow
private void SetSelectedDesktopComponent(Border? host) private void SetSelectedDesktopComponent(Border? host)
{ {
ClearSelectedLauncherTile(refreshTaskbar: false);
// Clear previous selection // Clear previous selection
if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host) if (_selectedDesktopComponentHost is not null && _selectedDesktopComponentHost != host)
{ {

View File

@@ -42,6 +42,9 @@ public partial class MainWindow
private readonly Stack<StartMenuFolderNode> _launcherFolderStack = []; private readonly Stack<StartMenuFolderNode> _launcherFolderStack = [];
private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _hiddenLauncherFolderPaths = new(StringComparer.OrdinalIgnoreCase);
private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase); private readonly HashSet<string> _hiddenLauncherAppPaths = new(StringComparer.OrdinalIgnoreCase);
private Button? _selectedLauncherTileButton;
private LauncherEntryKind? _selectedLauncherEntryKind;
private string? _selectedLauncherEntryKey;
private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty); private StartMenuFolderNode _startMenuRoot = new("All Apps", string.Empty);
private byte[]? _launcherFolderIconPngBytes; private byte[]? _launcherFolderIconPngBytes;
private Bitmap? _launcherFolderIconBitmap; private Bitmap? _launcherFolderIconBitmap;
@@ -341,6 +344,7 @@ public partial class MainWindow
if (_currentDesktopSurfaceIndex != LauncherSurfaceIndex) if (_currentDesktopSurfaceIndex != LauncherSurfaceIndex)
{ {
CloseLauncherFolderOverlay(); CloseLauncherFolderOverlay();
ClearSelectedLauncherTile(refreshTaskbar: false);
} }
UpdateDesktopPageAwareComponentContext(); UpdateDesktopPageAwareComponentContext();
@@ -386,12 +390,14 @@ public partial class MainWindow
return; return;
} }
// 如果在组件编辑模式下点击空白区域,取消组件选中 // 如果在组件编辑模式下点击空白区域,取消选中(组件或启动台图标)
if (_isComponentLibraryOpen && _selectedDesktopComponentHost is not null) if (_isComponentLibraryOpen &&
(_selectedDesktopComponentHost is not null || _selectedLauncherTileButton is not null))
{ {
if (!IsInteractivePointerSource(e.Source)) if (!IsInteractivePointerSource(e.Source))
{ {
ClearDesktopComponentSelection(); ClearDesktopComponentSelection();
ClearSelectedLauncherTile(refreshTaskbar: false);
ApplyTaskbarActionVisibility(GetCurrentTaskbarContext()); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
} }
} }
@@ -737,6 +743,7 @@ public partial class MainWindow
return; return;
} }
ClearSelectedLauncherTile(refreshTaskbar: false);
LauncherRootTilePanel.Children.Clear(); LauncherRootTilePanel.Children.Clear();
var folders = _startMenuRoot.Folders; var folders = _startMenuRoot.Folders;
var apps = _startMenuRoot.Apps; var apps = _startMenuRoot.Apps;
@@ -784,7 +791,8 @@ public partial class MainWindow
monogram: "DIR", monogram: "DIR",
iconBitmap: folderIconBitmap, iconBitmap: folderIconBitmap,
() => OpenLauncherFolder(folder), () => OpenLauncherFolder(folder),
hideAction: string.IsNullOrWhiteSpace(folderKey) ? null : () => HideLauncherFolder(folder)); LauncherEntryKind.Folder,
folderKey);
} }
private Button CreateLauncherAppTile(StartMenuAppEntry app) private Button CreateLauncherAppTile(StartMenuAppEntry app)
@@ -798,7 +806,8 @@ public partial class MainWindow
monogram, monogram,
iconBitmap, iconBitmap,
() => LaunchStartMenuEntry(app), () => LaunchStartMenuEntry(app),
hideAction: string.IsNullOrWhiteSpace(appKey) ? null : () => HideLauncherApp(app)); LauncherEntryKind.Shortcut,
appKey);
} }
private Control CreateLauncherHintTile(string title, string subtitle) private Control CreateLauncherHintTile(string title, string subtitle)
@@ -842,7 +851,8 @@ public partial class MainWindow
string monogram, string monogram,
Bitmap? iconBitmap, Bitmap? iconBitmap,
Action clickAction, Action clickAction,
Action? hideAction = null) LauncherEntryKind entryKind,
string entryKey)
{ {
Control iconControl = iconBitmap is not null Control iconControl = iconBitmap is not null
? new Image ? new Image
@@ -910,13 +920,26 @@ public partial class MainWindow
Classes = { "glass-panel" }, Classes = { "glass-panel" },
Margin = new Thickness(0, 0, 12, 12), Margin = new Thickness(0, 0, 12, 12),
BorderThickness = new Thickness(0), BorderThickness = new Thickness(0),
BorderBrush = Brushes.Transparent,
CornerRadius = new CornerRadius(20), CornerRadius = new CornerRadius(20),
Padding = new Thickness(10), Padding = new Thickness(10),
Content = content Content = content
// 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置 // 不设置固定 Width 和 Height由 UpdateLauncherTileLayout 动态设置
}; };
button.Click += (_, _) => clickAction(); button.Click += (_, _) =>
AttachLauncherTileContextMenu(button, hideAction); {
if (_isComponentLibraryOpen)
{
if (!string.IsNullOrWhiteSpace(entryKey))
{
SetSelectedLauncherTile(button, entryKind, entryKey);
}
return;
}
clickAction();
};
return button; return button;
} }
@@ -937,49 +960,100 @@ public partial class MainWindow
return string.IsNullOrWhiteSpace(key) || !_hiddenLauncherAppPaths.Contains(key); 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; 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(); if (changed)
contextMenu.Items.Add(hideItem);
tileButton.ContextMenu = contextMenu;
}
private void HideLauncherFolder(StartMenuFolderNode folder)
{
var key = NormalizeLauncherHiddenKey(folder.RelativePath);
if (string.IsNullOrWhiteSpace(key) || !_hiddenLauncherFolderPaths.Add(key))
{ {
ApplyLauncherVisibilitySettingsChange();
return; return;
} }
ApplyLauncherVisibilitySettingsChange(); ApplyTaskbarActionVisibility(GetCurrentTaskbarContext());
}
private void HideLauncherApp(StartMenuAppEntry app)
{
var key = NormalizeLauncherHiddenKey(app.RelativePath);
if (string.IsNullOrWhiteSpace(key) || !_hiddenLauncherAppPaths.Add(key))
{
return;
}
ApplyLauncherVisibilitySettingsChange();
} }
private void ApplyLauncherVisibilitySettingsChange() private void ApplyLauncherVisibilitySettingsChange()
{ {
ClearSelectedLauncherTile(refreshTaskbar: false);
RenderLauncherRootTiles(); RenderLauncherRootTiles();
if (_launcherFolderStack.Count > 0) if (_launcherFolderStack.Count > 0)
{ {
@@ -1278,6 +1352,7 @@ public partial class MainWindow
private void CloseLauncherFolderOverlay() private void CloseLauncherFolderOverlay()
{ {
ClearSelectedLauncherTile(refreshTaskbar: false);
_launcherFolderStack.Clear(); _launcherFolderStack.Clear();
if (LauncherFolderOverlay is not null) if (LauncherFolderOverlay is not null)
{ {
@@ -1300,6 +1375,7 @@ public partial class MainWindow
return; return;
} }
ClearSelectedLauncherTile(refreshTaskbar: false);
if (_launcherFolderStack.Count == 0) if (_launcherFolderStack.Count == 0)
{ {
CloseLauncherFolderOverlay(); CloseLauncherFolderOverlay();