mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
0.4.6
引入了托盘菜单,提供了应用启动台隐藏应用功能,优化了自动刷新功能,为STCN 24组件提供了更多信息选项。
This commit is contained in:
@@ -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 />
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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": "央广网设置",
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
74
LanMountainDesktop/Models/RefreshIntervalCatalog.cs
Normal file
74
LanMountainDesktop/Models/RefreshIntervalCatalog.cs
Normal 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"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
42
LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs
Normal file
42
LanMountainDesktop/Models/Stcn24ForumSourceTypes.cs
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,5 +7,6 @@ public enum TaskbarActionId
|
|||||||
AddDesktopPage,
|
AddDesktopPage,
|
||||||
DeleteDesktopPage,
|
DeleteDesktopPage,
|
||||||
DeleteComponent,
|
DeleteComponent,
|
||||||
EditComponent
|
EditComponent,
|
||||||
|
HideLauncherEntry
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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();
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user