mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 09:14:25 +08:00
0.4.3fixed
This commit is contained in:
@@ -251,8 +251,7 @@ public sealed class ComponentRegistry
|
|||||||
MinWidthCells: 4,
|
MinWidthCells: 4,
|
||||||
MinHeightCells: 2,
|
MinHeightCells: 2,
|
||||||
AllowStatusBarPlacement: false,
|
AllowStatusBarPlacement: false,
|
||||||
AllowDesktopPlacement: true,
|
AllowDesktopPlacement: true),
|
||||||
ResizeMode: DesktopComponentResizeMode.Free),
|
|
||||||
new DesktopComponentDefinition(
|
new DesktopComponentDefinition(
|
||||||
BuiltInComponentIds.DesktopWhiteboard,
|
BuiltInComponentIds.DesktopWhiteboard,
|
||||||
"Blackboard Portrait",
|
"Blackboard Portrait",
|
||||||
|
|||||||
@@ -337,6 +337,17 @@
|
|||||||
"dailysentence.widget.fallback_sentence": "Daily sentence is temporarily unavailable.",
|
"dailysentence.widget.fallback_sentence": "Daily sentence is temporarily unavailable.",
|
||||||
"dailysentence.widget.fallback_translation": "Tap refresh and try again.",
|
"dailysentence.widget.fallback_translation": "Tap refresh and try again.",
|
||||||
"dailysentence.widget.source_default": "Youdao Dictionary",
|
"dailysentence.widget.source_default": "Youdao Dictionary",
|
||||||
|
"daily_sentence.settings.title": "Daily Sentence Settings",
|
||||||
|
"daily_sentence.settings.desc": "Configure auto-rotation and refresh interval.",
|
||||||
|
"daily_sentence.settings.auto_rotate_label": "Auto-rotation",
|
||||||
|
"daily_sentence.settings.auto_rotate_enabled": "Enable auto-rotation",
|
||||||
|
"daily_sentence.settings.frequency_label": "Rotation interval",
|
||||||
|
"daily_sentence.settings.frequency_5m": "5 minutes",
|
||||||
|
"daily_sentence.settings.frequency_10m": "10 minutes",
|
||||||
|
"daily_sentence.settings.frequency_40m": "40 minutes",
|
||||||
|
"daily_sentence.settings.frequency_1h": "1 hour",
|
||||||
|
"daily_sentence.settings.frequency_12h": "12 hours",
|
||||||
|
"daily_sentence.settings.frequency_24h": "24 hours",
|
||||||
"cnrnews.widget.loading": "Loading...",
|
"cnrnews.widget.loading": "Loading...",
|
||||||
"cnrnews.widget.loading_title": "Fetching CNR headlines",
|
"cnrnews.widget.loading_title": "Fetching CNR headlines",
|
||||||
"cnrnews.widget.loading_subtitle": "Please wait",
|
"cnrnews.widget.loading_subtitle": "Please wait",
|
||||||
@@ -344,6 +355,17 @@
|
|||||||
"cnrnews.widget.fallback_title": "CNR news is temporarily unavailable",
|
"cnrnews.widget.fallback_title": "CNR news is temporarily unavailable",
|
||||||
"cnrnews.widget.fallback_subtitle": "Tap refresh and try again",
|
"cnrnews.widget.fallback_subtitle": "Tap refresh and try again",
|
||||||
"cnrnews.widget.hot_label": "Hot",
|
"cnrnews.widget.hot_label": "Hot",
|
||||||
|
"cnrnews.settings.title": "CNR Settings",
|
||||||
|
"cnrnews.settings.desc": "Configure auto-rotation and refresh interval.",
|
||||||
|
"cnrnews.settings.auto_rotate_label": "Auto-rotation",
|
||||||
|
"cnrnews.settings.auto_rotate_enabled": "Enable auto-rotation",
|
||||||
|
"cnrnews.settings.frequency_label": "Rotation interval",
|
||||||
|
"cnrnews.settings.frequency_5m": "5 minutes",
|
||||||
|
"cnrnews.settings.frequency_10m": "10 minutes",
|
||||||
|
"cnrnews.settings.frequency_40m": "40 minutes",
|
||||||
|
"cnrnews.settings.frequency_1h": "1 hour",
|
||||||
|
"cnrnews.settings.frequency_12h": "12 hours",
|
||||||
|
"cnrnews.settings.frequency_24h": "24 hours",
|
||||||
"artwork.settings.title": "Daily Artwork Settings",
|
"artwork.settings.title": "Daily Artwork Settings",
|
||||||
"artwork.settings.desc": "Switch the data source used by Daily Artwork.",
|
"artwork.settings.desc": "Switch the data source used by Daily Artwork.",
|
||||||
"artwork.settings.source_label": "Mirror Source",
|
"artwork.settings.source_label": "Mirror Source",
|
||||||
|
|||||||
@@ -337,6 +337,17 @@
|
|||||||
"dailysentence.widget.fallback_sentence": "今日英语句子暂不可用",
|
"dailysentence.widget.fallback_sentence": "今日英语句子暂不可用",
|
||||||
"dailysentence.widget.fallback_translation": "请点击右上角刷新重试",
|
"dailysentence.widget.fallback_translation": "请点击右上角刷新重试",
|
||||||
"dailysentence.widget.source_default": "有道词典",
|
"dailysentence.widget.source_default": "有道词典",
|
||||||
|
"daily_sentence.settings.title": "英语句子设置",
|
||||||
|
"daily_sentence.settings.desc": "配置自动轮换与刷新频率。",
|
||||||
|
"daily_sentence.settings.auto_rotate_label": "自动轮换",
|
||||||
|
"daily_sentence.settings.auto_rotate_enabled": "启用自动轮换",
|
||||||
|
"daily_sentence.settings.frequency_label": "轮换频率",
|
||||||
|
"daily_sentence.settings.frequency_5m": "5 分钟",
|
||||||
|
"daily_sentence.settings.frequency_10m": "10 分钟",
|
||||||
|
"daily_sentence.settings.frequency_40m": "40 分钟",
|
||||||
|
"daily_sentence.settings.frequency_1h": "1 小时",
|
||||||
|
"daily_sentence.settings.frequency_12h": "12 小时",
|
||||||
|
"daily_sentence.settings.frequency_24h": "24 小时",
|
||||||
"cnrnews.widget.loading": "加载中...",
|
"cnrnews.widget.loading": "加载中...",
|
||||||
"cnrnews.widget.loading_title": "正在获取新闻热点",
|
"cnrnews.widget.loading_title": "正在获取新闻热点",
|
||||||
"cnrnews.widget.loading_subtitle": "请稍候",
|
"cnrnews.widget.loading_subtitle": "请稍候",
|
||||||
@@ -344,6 +355,17 @@
|
|||||||
"cnrnews.widget.fallback_title": "央广网新闻暂不可用",
|
"cnrnews.widget.fallback_title": "央广网新闻暂不可用",
|
||||||
"cnrnews.widget.fallback_subtitle": "点击右上角稍后重试",
|
"cnrnews.widget.fallback_subtitle": "点击右上角稍后重试",
|
||||||
"cnrnews.widget.hot_label": "热点",
|
"cnrnews.widget.hot_label": "热点",
|
||||||
|
"cnrnews.settings.title": "央广网设置",
|
||||||
|
"cnrnews.settings.desc": "配置新闻自动轮换与刷新频率。",
|
||||||
|
"cnrnews.settings.auto_rotate_label": "自动轮换",
|
||||||
|
"cnrnews.settings.auto_rotate_enabled": "启用自动轮换",
|
||||||
|
"cnrnews.settings.frequency_label": "轮换频率",
|
||||||
|
"cnrnews.settings.frequency_5m": "5 分钟",
|
||||||
|
"cnrnews.settings.frequency_10m": "10 分钟",
|
||||||
|
"cnrnews.settings.frequency_40m": "40 分钟",
|
||||||
|
"cnrnews.settings.frequency_1h": "1 小时",
|
||||||
|
"cnrnews.settings.frequency_12h": "12 小时",
|
||||||
|
"cnrnews.settings.frequency_24h": "24 小时",
|
||||||
"artwork.settings.title": "每日图片设置",
|
"artwork.settings.title": "每日图片设置",
|
||||||
"artwork.settings.desc": "切换每日图片的数据源。",
|
"artwork.settings.desc": "切换每日图片的数据源。",
|
||||||
"artwork.settings.source_label": "镜像源",
|
"artwork.settings.source_label": "镜像源",
|
||||||
|
|||||||
@@ -98,6 +98,14 @@ public sealed class AppSettingsSnapshot
|
|||||||
];
|
];
|
||||||
public string WorldClockSecondHandMode { get; set; } = "Tick";
|
public string WorldClockSecondHandMode { get; set; } = "Tick";
|
||||||
|
|
||||||
|
public bool DailySentenceAutoRotateEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
public int DailySentenceAutoRotateIntervalMinutes { get; set; } = 60;
|
||||||
|
|
||||||
|
public bool CnrDailyNewsAutoRotateEnabled { get; set; } = true;
|
||||||
|
|
||||||
|
public int CnrDailyNewsAutoRotateIntervalMinutes { get; set; } = 60;
|
||||||
|
|
||||||
public AppSettingsSnapshot Clone()
|
public AppSettingsSnapshot Clone()
|
||||||
{
|
{
|
||||||
var clone = (AppSettingsSnapshot)MemberwiseClone();
|
var clone = (AppSettingsSnapshot)MemberwiseClone();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -53,6 +53,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
private DailyPoetryCacheEntry? _dailyPoetryCache;
|
private DailyPoetryCacheEntry? _dailyPoetryCache;
|
||||||
private DailyNewsCacheEntry? _dailyNewsCache;
|
private DailyNewsCacheEntry? _dailyNewsCache;
|
||||||
private DailyWordCacheEntry? _dailyWordCache;
|
private DailyWordCacheEntry? _dailyWordCache;
|
||||||
|
private int _dailyNewsRotationCursor;
|
||||||
|
|
||||||
static RecommendationDataService()
|
static RecommendationDataService()
|
||||||
{
|
{
|
||||||
@@ -206,10 +207,10 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
"No CNR news items were returned.");
|
"No CNR news items were returned.");
|
||||||
}
|
}
|
||||||
|
|
||||||
var snapshot = new DailyNewsSnapshot(
|
var snapshot = new DailyNewsSnapshot(
|
||||||
Provider: "CNR",
|
Provider: "CNR",
|
||||||
Source: "央广网·头条",
|
Source: "央广网·头条",
|
||||||
Items: items.Take(targetCount).ToArray(),
|
Items: SelectDailyNewsItems(items, targetCount, normalizedQuery.ForceRefresh),
|
||||||
FetchedAt: DateTimeOffset.UtcNow);
|
FetchedAt: DateTimeOffset.UtcNow);
|
||||||
|
|
||||||
SetDailyNewsCache(snapshot);
|
SetDailyNewsCache(snapshot);
|
||||||
@@ -521,6 +522,33 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<DailyNewsItemSnapshot> SelectDailyNewsItems(
|
||||||
|
IReadOnlyList<DailyNewsItemSnapshot> items,
|
||||||
|
int targetCount,
|
||||||
|
bool forceRefresh)
|
||||||
|
{
|
||||||
|
if (items.Count == 0 || targetCount <= 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var safeCount = Math.Min(targetCount, items.Count);
|
||||||
|
if (!forceRefresh || items.Count <= safeCount)
|
||||||
|
{
|
||||||
|
return items.Take(safeCount).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
var cursor = Math.Abs(Interlocked.Increment(ref _dailyNewsRotationCursor) - 1);
|
||||||
|
var startIndex = cursor % items.Count;
|
||||||
|
var selection = new List<DailyNewsItemSnapshot>(safeCount);
|
||||||
|
for (var i = 0; i < safeCount; i++)
|
||||||
|
{
|
||||||
|
selection.Add(items[(startIndex + i) % items.Count]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return selection;
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryGetDailyWordFromCache(out DailyWordSnapshot snapshot)
|
private bool TryGetDailyWordFromCache(out DailyWordSnapshot snapshot)
|
||||||
{
|
{
|
||||||
lock (_cacheGate)
|
lock (_cacheGate)
|
||||||
@@ -723,7 +751,7 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Join(";", lines
|
return string.Join("; ", lines
|
||||||
.Where(line => !string.IsNullOrWhiteSpace(line))
|
.Where(line => !string.IsNullOrWhiteSpace(line))
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Take(3));
|
.Take(3));
|
||||||
@@ -1646,3 +1674,4 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
|||||||
: $"{text[..maxLength]}...";
|
: $"{text[..maxLength]}...";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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.CnrDailyNewsSettingsWindow">
|
||||||
|
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
|
||||||
|
Padding="16">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*"
|
||||||
|
RowSpacing="10">
|
||||||
|
<TextBlock x:Name="TitleTextBlock"
|
||||||
|
Text="CNR news settings"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="Configure auto-rotation 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="AutoRotateLabelTextBlock"
|
||||||
|
Text="Auto-rotation"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<CheckBox x:Name="AutoRotateCheckBox"
|
||||||
|
Content="Enable auto-rotation"
|
||||||
|
Checked="OnAutoRotateChanged"
|
||||||
|
Unchecked="OnAutoRotateChanged" />
|
||||||
|
</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="Rotation 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="Frequency40mItem"
|
||||||
|
Tag="40"
|
||||||
|
Content="40 min" />
|
||||||
|
<ComboBoxItem x:Name="Frequency1hItem"
|
||||||
|
Tag="60"
|
||||||
|
Content="1 hour" />
|
||||||
|
<ComboBoxItem x:Name="Frequency12hItem"
|
||||||
|
Tag="720"
|
||||||
|
Content="12 hours" />
|
||||||
|
<ComboBoxItem x:Name="Frequency24hItem"
|
||||||
|
Tag="1440"
|
||||||
|
Content="24 hours" />
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class CnrDailyNewsSettingsWindow : UserControl
|
||||||
|
{
|
||||||
|
private static readonly int[] SupportedIntervals = [5, 10, 40, 60, 720, 1440];
|
||||||
|
|
||||||
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private bool _suppressEvents;
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
|
||||||
|
public event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
|
public CnrDailyNewsSettingsWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
LoadState();
|
||||||
|
ApplyLocalization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
|
||||||
|
var enabled = snapshot.CnrDailyNewsAutoRotateEnabled;
|
||||||
|
var interval = NormalizeInterval(snapshot.CnrDailyNewsAutoRotateIntervalMinutes);
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
AutoRotateCheckBox.IsChecked = enabled;
|
||||||
|
SelectInterval(interval);
|
||||||
|
FrequencyCardBorder.IsVisible = enabled;
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
TitleTextBlock.Text = L("cnrnews.settings.title", "CNR news settings");
|
||||||
|
DescriptionTextBlock.Text = L("cnrnews.settings.desc", "Configure auto-rotation and refresh interval.");
|
||||||
|
AutoRotateLabelTextBlock.Text = L("cnrnews.settings.auto_rotate_label", "Auto-rotation");
|
||||||
|
AutoRotateCheckBox.Content = L("cnrnews.settings.auto_rotate_enabled", "Enable auto-rotation");
|
||||||
|
FrequencyLabelTextBlock.Text = L("cnrnews.settings.frequency_label", "Rotation interval");
|
||||||
|
Frequency5mItem.Content = L("cnrnews.settings.frequency_5m", "5 min");
|
||||||
|
Frequency10mItem.Content = L("cnrnews.settings.frequency_10m", "10 min");
|
||||||
|
Frequency40mItem.Content = L("cnrnews.settings.frequency_40m", "40 min");
|
||||||
|
Frequency1hItem.Content = L("cnrnews.settings.frequency_1h", "1 hour");
|
||||||
|
Frequency12hItem.Content = L("cnrnews.settings.frequency_12h", "12 hours");
|
||||||
|
Frequency24hItem.Content = L("cnrnews.settings.frequency_24h", "24 hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAutoRotateChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabled = AutoRotateCheckBox.IsChecked == true;
|
||||||
|
FrequencyCardBorder.IsVisible = enabled;
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
snapshot.CnrDailyNewsAutoRotateEnabled = AutoRotateCheckBox.IsChecked == true;
|
||||||
|
snapshot.CnrDailyNewsAutoRotateIntervalMinutes = GetSelectedInterval();
|
||||||
|
_appSettingsService.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 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (minutes <= 0)
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SupportedIntervals.Contains(minutes))
|
||||||
|
{
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedIntervals
|
||||||
|
.OrderBy(value => Math.Abs(value - minutes))
|
||||||
|
.FirstOrDefault(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:fi="using:FluentIcons.Avalonia"
|
||||||
mc:Ignorable="d"
|
mc:Ignorable="d"
|
||||||
d:DesignWidth="640"
|
d:DesignWidth="640"
|
||||||
d:DesignHeight="320"
|
d:DesignHeight="320"
|
||||||
@@ -56,12 +57,12 @@
|
|||||||
Spacing="4"
|
Spacing="4"
|
||||||
HorizontalAlignment="Center"
|
HorizontalAlignment="Center"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<TextBlock x:Name="RefreshGlyphTextBlock"
|
<fi:SymbolIcon x:Name="RefreshGlyphIcon"
|
||||||
Text="↻"
|
Symbol="ArrowClockwise"
|
||||||
Foreground="#52575F"
|
IconVariant="Regular"
|
||||||
FontSize="19"
|
Foreground="#52575F"
|
||||||
FontWeight="SemiBold"
|
FontSize="19"
|
||||||
VerticalAlignment="Center" />
|
VerticalAlignment="Center" />
|
||||||
<TextBlock x:Name="RefreshLabelTextBlock"
|
<TextBlock x:Name="RefreshLabelTextBlock"
|
||||||
Text="换一换"
|
Text="换一换"
|
||||||
Foreground="#202327"
|
Foreground="#202327"
|
||||||
|
|||||||
@@ -36,6 +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 readonly DispatcherTimer _refreshTimer = new()
|
private readonly DispatcherTimer _refreshTimer = new()
|
||||||
{
|
{
|
||||||
@@ -85,6 +86,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private double _currentCellSize = BaseCellSize;
|
private double _currentCellSize = BaseCellSize;
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
|
private bool _autoRotateEnabled = true;
|
||||||
|
|
||||||
public CnrDailyNewsWidget()
|
public CnrDailyNewsWidget()
|
||||||
{
|
{
|
||||||
@@ -92,7 +94,6 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
BrandPrimaryTextBlock.FontFamily = MiSansFontFamily;
|
BrandPrimaryTextBlock.FontFamily = MiSansFontFamily;
|
||||||
BrandSecondaryTextBlock.FontFamily = MiSansFontFamily;
|
BrandSecondaryTextBlock.FontFamily = MiSansFontFamily;
|
||||||
RefreshGlyphTextBlock.FontFamily = MiSansFontFamily;
|
|
||||||
RefreshLabelTextBlock.FontFamily = MiSansFontFamily;
|
RefreshLabelTextBlock.FontFamily = MiSansFontFamily;
|
||||||
News1TitleTextBlock.FontFamily = MiSansFontFamily;
|
News1TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||||
News2TitleTextBlock.FontFamily = MiSansFontFamily;
|
News2TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||||
@@ -106,6 +107,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
ApplyLoadingState();
|
ApplyLoadingState();
|
||||||
UpdateRefreshButtonState();
|
UpdateRefreshButtonState();
|
||||||
}
|
}
|
||||||
@@ -128,6 +130,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
public void RefreshFromSettings()
|
public void RefreshFromSettings()
|
||||||
{
|
{
|
||||||
_recommendationService.ClearCache();
|
_recommendationService.ClearCache();
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
if (_isAttached)
|
if (_isAttached)
|
||||||
{
|
{
|
||||||
_ = RefreshNewsAsync(forceRefresh: true);
|
_ = RefreshNewsAsync(forceRefresh: true);
|
||||||
@@ -137,8 +140,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
_isAttached = true;
|
_isAttached = true;
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
UpdateRefreshButtonState();
|
UpdateRefreshButtonState();
|
||||||
_refreshTimer.Start();
|
|
||||||
_ = RefreshNewsAsync(forceRefresh: false);
|
_ = RefreshNewsAsync(forceRefresh: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,26 +158,6 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||||
{
|
{
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
var desiredCount = ResolveDesiredNewsItemCount();
|
|
||||||
var previousRenderedCount = _renderedNewsCount;
|
|
||||||
if (_activeNewsItems.Count > 0 && desiredCount != previousRenderedCount)
|
|
||||||
{
|
|
||||||
RenderExtraNewsRows(_activeNewsItems.Take(desiredCount).Skip(2).ToArray());
|
|
||||||
UpdateNewsInteractionState();
|
|
||||||
}
|
|
||||||
|
|
||||||
var shouldFetchMoreItems = desiredCount > _activeNewsItems.Count;
|
|
||||||
var shouldReloadExpandedImages =
|
|
||||||
desiredCount > previousRenderedCount &&
|
|
||||||
desiredCount <= _activeNewsItems.Count;
|
|
||||||
|
|
||||||
if (_isAttached &&
|
|
||||||
!_isRefreshing &&
|
|
||||||
_activeNewsItems.Count > 0 &&
|
|
||||||
(shouldFetchMoreItems || shouldReloadExpandedImages))
|
|
||||||
{
|
|
||||||
_ = RefreshNewsAsync(forceRefresh: false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||||
@@ -190,7 +173,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await RefreshNewsAsync(forceRefresh: false);
|
await RefreshNewsAsync(forceRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnNewsItem1PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
@@ -290,10 +273,9 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private async Task ApplySnapshotAsync(DailyNewsSnapshot snapshot, CancellationToken cancellationToken)
|
private async Task ApplySnapshotAsync(DailyNewsSnapshot snapshot, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var desiredCount = ResolveDesiredNewsItemCount();
|
|
||||||
var items = snapshot.Items is null
|
var items = snapshot.Items is null
|
||||||
? []
|
? []
|
||||||
: snapshot.Items.Take(desiredCount).ToArray();
|
: snapshot.Items.Take(2).ToArray();
|
||||||
_activeNewsItems = items;
|
_activeNewsItems = items;
|
||||||
|
|
||||||
var item1 = items.Length > 0 ? items[0] : null;
|
var item1 = items.Length > 0 ? items[0] : null;
|
||||||
@@ -308,52 +290,27 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
_newsUrls.Add(NormalizeHttpUrl(item.Url));
|
_newsUrls.Add(NormalizeHttpUrl(item.Url));
|
||||||
}
|
}
|
||||||
|
|
||||||
RenderExtraNewsRows(items.Skip(2).ToArray());
|
RenderExtraNewsRows([]);
|
||||||
UpdateNewsInteractionState();
|
UpdateNewsInteractionState();
|
||||||
|
|
||||||
StatusTextBlock.IsVisible = false;
|
StatusTextBlock.IsVisible = false;
|
||||||
UpdateAdaptiveLayout();
|
UpdateAdaptiveLayout();
|
||||||
|
|
||||||
var loadTasks = items
|
var loadTasks = new[]
|
||||||
.Select(item => TryDownloadBitmapAsync(item.ImageUrl, cancellationToken))
|
{
|
||||||
.ToArray();
|
TryDownloadBitmapAsync(item1?.ImageUrl, cancellationToken),
|
||||||
|
TryDownloadBitmapAsync(item2?.ImageUrl, cancellationToken)
|
||||||
|
};
|
||||||
var bitmaps = await Task.WhenAll(loadTasks);
|
var bitmaps = await Task.WhenAll(loadTasks);
|
||||||
if (cancellationToken.IsCancellationRequested || !_isAttached)
|
if (cancellationToken.IsCancellationRequested || !_isAttached)
|
||||||
{
|
{
|
||||||
foreach (var bitmap in bitmaps)
|
bitmaps[0]?.Dispose();
|
||||||
{
|
bitmaps[1]?.Dispose();
|
||||||
bitmap?.Dispose();
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var consumed = new bool[bitmaps.Length];
|
SetNewsBitmap(0, bitmaps[0]);
|
||||||
Bitmap? TakeBitmapAt(int index)
|
SetNewsBitmap(1, bitmaps[1]);
|
||||||
{
|
|
||||||
if (index < 0 || index >= bitmaps.Length)
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
consumed[index] = true;
|
|
||||||
return bitmaps[index];
|
|
||||||
}
|
|
||||||
|
|
||||||
SetNewsBitmap(0, TakeBitmapAt(0));
|
|
||||||
SetNewsBitmap(1, TakeBitmapAt(1));
|
|
||||||
|
|
||||||
for (var rowIndex = 0; rowIndex < _extraNewsRows.Count; rowIndex++)
|
|
||||||
{
|
|
||||||
SetExtraNewsBitmap(rowIndex, TakeBitmapAt(rowIndex + 2));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var i = 0; i < bitmaps.Length; i++)
|
|
||||||
{
|
|
||||||
if (!consumed[i])
|
|
||||||
{
|
|
||||||
bitmaps[i]?.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplyLoadingState()
|
private void ApplyLoadingState()
|
||||||
@@ -389,24 +346,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private int ResolveDesiredNewsItemCount()
|
private int ResolveDesiredNewsItemCount()
|
||||||
{
|
{
|
||||||
var span = ResolveCurrentCellSpan();
|
return 2;
|
||||||
var baseEquivalentHeight = span.HeightCells * (double)BaseWidthCells / Math.Max(BaseWidthCells, span.WidthCells);
|
|
||||||
var effectiveHeightCells = (int)Math.Round(baseEquivalentHeight, MidpointRounding.AwayFromZero);
|
|
||||||
return Math.Clamp(Math.Max(BaseHeightCells, effectiveHeightCells), 2, 12);
|
|
||||||
}
|
|
||||||
|
|
||||||
private (int WidthCells, int HeightCells) ResolveCurrentCellSpan()
|
|
||||||
{
|
|
||||||
var pitch = Math.Max(1, _currentCellSize);
|
|
||||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
|
||||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
|
||||||
|
|
||||||
var normalizedWidth = totalWidth + Math.Clamp(pitch * 0.20, 6, 16);
|
|
||||||
var normalizedHeight = totalHeight + Math.Clamp(pitch * 0.18, 4, 12);
|
|
||||||
|
|
||||||
var widthCells = Math.Max(BaseWidthCells, (int)Math.Round(normalizedWidth / pitch, MidpointRounding.AwayFromZero));
|
|
||||||
var heightCells = Math.Max(BaseHeightCells, (int)Math.Round(normalizedHeight / pitch, MidpointRounding.AwayFromZero));
|
|
||||||
return (widthCells, heightCells);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateHotHeadlineText(string? title)
|
private void UpdateHotHeadlineText(string? title)
|
||||||
@@ -558,7 +498,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
RefreshButton.Height = refreshHeight;
|
RefreshButton.Height = refreshHeight;
|
||||||
RefreshButton.Width = refreshWidth;
|
RefreshButton.Width = refreshWidth;
|
||||||
RefreshButton.CornerRadius = new CornerRadius(refreshHeight / 2d);
|
RefreshButton.CornerRadius = new CornerRadius(refreshHeight / 2d);
|
||||||
RefreshGlyphTextBlock.FontSize = Math.Clamp(19 * scale, 11, 24);
|
RefreshGlyphIcon.FontSize = Math.Clamp(19 * scale, 11, 24);
|
||||||
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
|
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
|
||||||
|
|
||||||
var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170);
|
var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170);
|
||||||
@@ -621,7 +561,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
{
|
{
|
||||||
RefreshButton.IsEnabled = !_isRefreshing;
|
RefreshButton.IsEnabled = !_isRefreshing;
|
||||||
RefreshButton.Opacity = _isAttached ? 1.0 : 0.85;
|
RefreshButton.Opacity = _isAttached ? 1.0 : 0.85;
|
||||||
RefreshGlyphTextBlock.Opacity = _isRefreshing ? 0.56 : 1.0;
|
RefreshGlyphIcon.Opacity = _isRefreshing ? 0.56 : 1.0;
|
||||||
RefreshLabelTextBlock.Opacity = _isRefreshing ? 0.56 : 1.0;
|
RefreshLabelTextBlock.Opacity = _isRefreshing ? 0.56 : 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -774,6 +714,60 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyAutoRotateSettings()
|
||||||
|
{
|
||||||
|
var enabled = true;
|
||||||
|
var intervalMinutes = 60;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsService.Load();
|
||||||
|
enabled = snapshot.CnrDailyNewsAutoRotateEnabled;
|
||||||
|
intervalMinutes = NormalizeAutoRotateIntervalMinutes(snapshot.CnrDailyNewsAutoRotateIntervalMinutes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep fallback defaults.
|
||||||
|
}
|
||||||
|
|
||||||
|
_autoRotateEnabled = enabled;
|
||||||
|
_refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
|
||||||
|
|
||||||
|
if (!_isAttached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoRotateEnabled)
|
||||||
|
{
|
||||||
|
if (!_refreshTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_refreshTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_refreshTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_refreshTimer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeAutoRotateIntervalMinutes(int minutes)
|
||||||
|
{
|
||||||
|
if (minutes <= 0)
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SupportedAutoRotateIntervalsMinutes.Contains(minutes))
|
||||||
|
{
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedAutoRotateIntervalsMinutes
|
||||||
|
.OrderBy(value => Math.Abs(value - minutes))
|
||||||
|
.FirstOrDefault(60);
|
||||||
|
}
|
||||||
|
|
||||||
private void CancelRefreshRequest()
|
private void CancelRefreshRequest()
|
||||||
{
|
{
|
||||||
var cts = Interlocked.Exchange(ref _refreshCts, null);
|
var cts = Interlocked.Exchange(ref _refreshCts, null);
|
||||||
|
|||||||
@@ -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.DailySentenceSettingsWindow">
|
||||||
|
<Border Background="{DynamicResource AdaptiveBackgroundBrush}"
|
||||||
|
Padding="16">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,*"
|
||||||
|
RowSpacing="10">
|
||||||
|
<TextBlock x:Name="TitleTextBlock"
|
||||||
|
Text="Daily sentence settings"
|
||||||
|
FontSize="18"
|
||||||
|
FontWeight="SemiBold"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
|
||||||
|
<TextBlock x:Name="DescriptionTextBlock"
|
||||||
|
Grid.Row="1"
|
||||||
|
Text="Configure auto-rotation 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="AutoRotateLabelTextBlock"
|
||||||
|
Text="Auto-rotation"
|
||||||
|
Foreground="{DynamicResource AdaptiveTextPrimaryBrush}" />
|
||||||
|
<CheckBox x:Name="AutoRotateCheckBox"
|
||||||
|
Content="Enable auto-rotation"
|
||||||
|
Checked="OnAutoRotateChanged"
|
||||||
|
Unchecked="OnAutoRotateChanged" />
|
||||||
|
</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="Rotation 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="Frequency40mItem"
|
||||||
|
Tag="40"
|
||||||
|
Content="40 min" />
|
||||||
|
<ComboBoxItem x:Name="Frequency1hItem"
|
||||||
|
Tag="60"
|
||||||
|
Content="1 hour" />
|
||||||
|
<ComboBoxItem x:Name="Frequency12hItem"
|
||||||
|
Tag="720"
|
||||||
|
Content="12 hours" />
|
||||||
|
<ComboBoxItem x:Name="Frequency24hItem"
|
||||||
|
Tag="1440"
|
||||||
|
Content="24 hours" />
|
||||||
|
</ComboBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</UserControl>
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Interactivity;
|
||||||
|
using LanMountainDesktop.Services;
|
||||||
|
|
||||||
|
namespace LanMountainDesktop.Views.Components;
|
||||||
|
|
||||||
|
public partial class DailySentenceSettingsWindow : UserControl
|
||||||
|
{
|
||||||
|
private static readonly int[] SupportedIntervals = [5, 10, 40, 60, 720, 1440];
|
||||||
|
|
||||||
|
private readonly AppSettingsService _appSettingsService = new();
|
||||||
|
private readonly LocalizationService _localizationService = new();
|
||||||
|
private bool _suppressEvents;
|
||||||
|
private string _languageCode = "zh-CN";
|
||||||
|
|
||||||
|
public event EventHandler? SettingsChanged;
|
||||||
|
|
||||||
|
public DailySentenceSettingsWindow()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
LoadState();
|
||||||
|
ApplyLocalization();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||||
|
|
||||||
|
var enabled = snapshot.DailySentenceAutoRotateEnabled;
|
||||||
|
var interval = NormalizeInterval(snapshot.DailySentenceAutoRotateIntervalMinutes);
|
||||||
|
|
||||||
|
_suppressEvents = true;
|
||||||
|
AutoRotateCheckBox.IsChecked = enabled;
|
||||||
|
SelectInterval(interval);
|
||||||
|
FrequencyCardBorder.IsVisible = enabled;
|
||||||
|
_suppressEvents = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyLocalization()
|
||||||
|
{
|
||||||
|
TitleTextBlock.Text = L("daily_sentence.settings.title", "Daily sentence settings");
|
||||||
|
DescriptionTextBlock.Text = L("daily_sentence.settings.desc", "Configure auto-rotation and refresh interval.");
|
||||||
|
AutoRotateLabelTextBlock.Text = L("daily_sentence.settings.auto_rotate_label", "Auto-rotation");
|
||||||
|
AutoRotateCheckBox.Content = L("daily_sentence.settings.auto_rotate_enabled", "Enable auto-rotation");
|
||||||
|
FrequencyLabelTextBlock.Text = L("daily_sentence.settings.frequency_label", "Rotation interval");
|
||||||
|
Frequency5mItem.Content = L("daily_sentence.settings.frequency_5m", "5 min");
|
||||||
|
Frequency10mItem.Content = L("daily_sentence.settings.frequency_10m", "10 min");
|
||||||
|
Frequency40mItem.Content = L("daily_sentence.settings.frequency_40m", "40 min");
|
||||||
|
Frequency1hItem.Content = L("daily_sentence.settings.frequency_1h", "1 hour");
|
||||||
|
Frequency12hItem.Content = L("daily_sentence.settings.frequency_12h", "12 hours");
|
||||||
|
Frequency24hItem.Content = L("daily_sentence.settings.frequency_24h", "24 hours");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnAutoRotateChanged(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var enabled = AutoRotateCheckBox.IsChecked == true;
|
||||||
|
FrequencyCardBorder.IsVisible = enabled;
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnFrequencySelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||||
|
{
|
||||||
|
_ = sender;
|
||||||
|
_ = e;
|
||||||
|
if (_suppressEvents)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SaveState();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SaveState()
|
||||||
|
{
|
||||||
|
var snapshot = _appSettingsService.Load();
|
||||||
|
snapshot.DailySentenceAutoRotateEnabled = AutoRotateCheckBox.IsChecked == true;
|
||||||
|
snapshot.DailySentenceAutoRotateIntervalMinutes = GetSelectedInterval();
|
||||||
|
_appSettingsService.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 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
{
|
||||||
|
if (minutes <= 0)
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SupportedIntervals.Contains(minutes))
|
||||||
|
{
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedIntervals
|
||||||
|
.OrderBy(value => Math.Abs(value - minutes))
|
||||||
|
.FirstOrDefault(60);
|
||||||
|
}
|
||||||
|
|
||||||
|
private string L(string key, string fallback)
|
||||||
|
{
|
||||||
|
return _localizationService.GetString(_languageCode, key, fallback);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -35,7 +35,8 @@
|
|||||||
Margin="16,14,16,14"
|
Margin="16,14,16,14"
|
||||||
RowDefinitions="Auto,*,Auto"
|
RowDefinitions="Auto,*,Auto"
|
||||||
RowSpacing="8">
|
RowSpacing="8">
|
||||||
<Grid Grid.Row="0"
|
<Grid x:Name="HeaderGrid"
|
||||||
|
Grid.Row="0"
|
||||||
ColumnDefinitions="Auto,*,Auto"
|
ColumnDefinitions="Auto,*,Auto"
|
||||||
ColumnSpacing="8">
|
ColumnSpacing="8">
|
||||||
<TextBlock x:Name="DayTextBlock"
|
<TextBlock x:Name="DayTextBlock"
|
||||||
@@ -80,7 +81,8 @@
|
|||||||
</Button>
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Grid.Row="1"
|
<StackPanel x:Name="SentenceStack"
|
||||||
|
Grid.Row="1"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Spacing="8">
|
Spacing="8">
|
||||||
<TextBlock x:Name="SentenceTextBlock"
|
<TextBlock x:Name="SentenceTextBlock"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -37,6 +38,7 @@ public partial class DailySentenceWidget : 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 readonly DispatcherTimer _refreshTimer = new()
|
private readonly DispatcherTimer _refreshTimer = new()
|
||||||
{
|
{
|
||||||
@@ -54,6 +56,7 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private double _currentCellSize = BaseCellSize;
|
private double _currentCellSize = BaseCellSize;
|
||||||
private bool _isAttached;
|
private bool _isAttached;
|
||||||
private bool _isRefreshing;
|
private bool _isRefreshing;
|
||||||
|
private bool _autoRotateEnabled = true;
|
||||||
|
|
||||||
public DailySentenceWidget()
|
public DailySentenceWidget()
|
||||||
{
|
{
|
||||||
@@ -75,6 +78,7 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
ApplyCellSize(_currentCellSize);
|
ApplyCellSize(_currentCellSize);
|
||||||
UpdateLanguageCode();
|
UpdateLanguageCode();
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
UpdateDateText();
|
UpdateDateText();
|
||||||
ApplyLoadingState();
|
ApplyLoadingState();
|
||||||
UpdateRefreshButtonState();
|
UpdateRefreshButtonState();
|
||||||
@@ -98,6 +102,7 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
public void RefreshFromSettings()
|
public void RefreshFromSettings()
|
||||||
{
|
{
|
||||||
_recommendationService.ClearCache();
|
_recommendationService.ClearCache();
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
if (_isAttached)
|
if (_isAttached)
|
||||||
{
|
{
|
||||||
_ = RefreshSentenceAsync(forceRefresh: true);
|
_ = RefreshSentenceAsync(forceRefresh: true);
|
||||||
@@ -107,8 +112,8 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||||
{
|
{
|
||||||
_isAttached = true;
|
_isAttached = true;
|
||||||
|
ApplyAutoRotateSettings();
|
||||||
UpdateRefreshButtonState();
|
UpdateRefreshButtonState();
|
||||||
_refreshTimer.Start();
|
|
||||||
_ = RefreshSentenceAsync(forceRefresh: false);
|
_ = RefreshSentenceAsync(forceRefresh: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -139,7 +144,7 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
await RefreshSentenceAsync(forceRefresh: false);
|
await RefreshSentenceAsync(forceRefresh: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnSourceTextBlockPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
private void OnSourceTextBlockPointerPressed(object? sender, Avalonia.Input.PointerPressedEventArgs e)
|
||||||
@@ -400,6 +405,7 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
Math.Clamp(16 * scale, 8, 28),
|
Math.Clamp(16 * scale, 8, 28),
|
||||||
Math.Clamp(14 * scale, 7, 24));
|
Math.Clamp(14 * scale, 7, 24));
|
||||||
ContentGrid.RowSpacing = Math.Clamp(8 * scale, 4, 12);
|
ContentGrid.RowSpacing = Math.Clamp(8 * scale, 4, 12);
|
||||||
|
HeaderGrid.ColumnSpacing = Math.Clamp(8 * scale, 4, 14);
|
||||||
|
|
||||||
var refreshSize = Math.Clamp(42 * scale, 24, 54);
|
var refreshSize = Math.Clamp(42 * scale, 24, 54);
|
||||||
RefreshButton.Width = refreshSize;
|
RefreshButton.Width = refreshSize;
|
||||||
@@ -409,14 +415,36 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
|
|
||||||
var innerWidth = Math.Max(100, totalWidth - ContentGrid.Margin.Left - ContentGrid.Margin.Right);
|
var innerWidth = Math.Max(100, totalWidth - ContentGrid.Margin.Left - ContentGrid.Margin.Right);
|
||||||
var innerHeight = Math.Max(56, totalHeight - ContentGrid.Margin.Top - ContentGrid.Margin.Bottom);
|
var innerHeight = Math.Max(56, totalHeight - ContentGrid.Margin.Top - ContentGrid.Margin.Bottom);
|
||||||
|
var rowSpacingTotal = ContentGrid.RowSpacing * 2d;
|
||||||
|
var availableRowsHeight = Math.Max(1, innerHeight - rowSpacingTotal);
|
||||||
|
|
||||||
var topRowHeight = Math.Max(20, innerHeight * 0.22);
|
var topRowHeight = Math.Clamp(innerHeight * 0.24, 18, 98);
|
||||||
var bottomRowHeight = Math.Max(14, innerHeight * 0.14);
|
var bottomRowHeight = Math.Clamp(innerHeight * 0.15, 11, 54);
|
||||||
var middleHeight = Math.Max(24, innerHeight - topRowHeight - bottomRowHeight - ContentGrid.RowSpacing * 2);
|
var minTopRowHeight = Math.Max(Math.Clamp(14 * scale, 10, 28), refreshSize * 0.60);
|
||||||
|
var minBottomRowHeight = Math.Clamp(10 * scale, 7, 22);
|
||||||
|
var minMiddleRowHeight = Math.Clamp(22 * scale, 14, 46);
|
||||||
|
|
||||||
|
if (topRowHeight + bottomRowHeight + minMiddleRowHeight > availableRowsHeight)
|
||||||
|
{
|
||||||
|
var scaling = availableRowsHeight / Math.Max(1, topRowHeight + bottomRowHeight + minMiddleRowHeight);
|
||||||
|
topRowHeight = Math.Max(minTopRowHeight, topRowHeight * scaling);
|
||||||
|
bottomRowHeight = Math.Max(minBottomRowHeight, bottomRowHeight * scaling);
|
||||||
|
}
|
||||||
|
|
||||||
|
var middleHeight = Math.Max(
|
||||||
|
minMiddleRowHeight,
|
||||||
|
availableRowsHeight - topRowHeight - bottomRowHeight);
|
||||||
|
|
||||||
|
if (ContentGrid.RowDefinitions.Count >= 3)
|
||||||
|
{
|
||||||
|
ContentGrid.RowDefinitions[0].Height = new GridLength(topRowHeight, GridUnitType.Pixel);
|
||||||
|
ContentGrid.RowDefinitions[1].Height = new GridLength(middleHeight, GridUnitType.Pixel);
|
||||||
|
ContentGrid.RowDefinitions[2].Height = new GridLength(bottomRowHeight, GridUnitType.Pixel);
|
||||||
|
}
|
||||||
|
|
||||||
var topTextWidth = Math.Max(76, innerWidth - refreshSize - ContentGrid.RowSpacing);
|
var topTextWidth = Math.Max(76, innerWidth - refreshSize - ContentGrid.RowSpacing);
|
||||||
var dayWidth = Math.Max(20, topTextWidth * 0.16);
|
var dayWidth = Math.Clamp(topTextWidth * 0.18, 20, Math.Max(24, topTextWidth * 0.32));
|
||||||
var monthYearWidth = Math.Max(48, topTextWidth - dayWidth - 6 * scale);
|
var monthYearWidth = Math.Max(48, topTextWidth - dayWidth - Math.Clamp(6 * scale, 2, 12));
|
||||||
DayTextBlock.MaxWidth = dayWidth;
|
DayTextBlock.MaxWidth = dayWidth;
|
||||||
MonthYearTextBlock.MaxWidth = monthYearWidth;
|
MonthYearTextBlock.MaxWidth = monthYearWidth;
|
||||||
|
|
||||||
@@ -448,17 +476,50 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
MonthYearTextBlock.FontWeight = monthLayout.Weight;
|
MonthYearTextBlock.FontWeight = monthLayout.Weight;
|
||||||
MonthYearTextBlock.LineHeight = monthLayout.LineHeight;
|
MonthYearTextBlock.LineHeight = monthLayout.LineHeight;
|
||||||
|
|
||||||
var sentenceLineLimit = innerHeight < _currentCellSize * 1.78 ? 2 : 3;
|
var sentenceTextDemand = Math.Clamp(NormalizeCompactText(SentenceTextBlock.Text).Length, 12, 360);
|
||||||
var sentenceHeight = Math.Max(16, middleHeight * 0.66);
|
var translationTextDemand = Math.Clamp(NormalizeCompactText(TranslationTextBlock.Text).Length, 8, 260);
|
||||||
var translationHeight = Math.Max(14, middleHeight - sentenceHeight - Math.Clamp(8 * scale, 3, 12));
|
|
||||||
|
var sentenceMinLines = innerHeight >= _currentCellSize * 1.78 ? 2 : 1;
|
||||||
|
var sentenceMaxLines = innerHeight >= _currentCellSize * 2.7
|
||||||
|
? 4
|
||||||
|
: innerHeight >= _currentCellSize * 1.92
|
||||||
|
? 3
|
||||||
|
: 2;
|
||||||
|
var translationMaxLines = innerHeight >= _currentCellSize * 2.35 ? 3 : 2;
|
||||||
|
|
||||||
|
var sentenceMinFont = Math.Clamp(23 * scale, 10, 42);
|
||||||
|
var translationMinFont = Math.Clamp(16 * scale, 8.5, 30);
|
||||||
|
var sentenceMinHeight = sentenceMinFont * 1.06 * sentenceMinLines;
|
||||||
|
var translationMinHeight = translationMinFont * 1.06;
|
||||||
|
var bodyGap = Math.Clamp(8 * scale, 3, 12);
|
||||||
|
SentenceStack.Spacing = bodyGap;
|
||||||
|
var minBodyHeight = sentenceMinHeight + translationMinHeight + bodyGap;
|
||||||
|
|
||||||
|
double sentenceHeight;
|
||||||
|
double translationHeight;
|
||||||
|
if (middleHeight <= minBodyHeight + 0.6)
|
||||||
|
{
|
||||||
|
var compression = middleHeight / Math.Max(1, minBodyHeight);
|
||||||
|
sentenceHeight = Math.Max(10, sentenceMinHeight * compression);
|
||||||
|
translationHeight = Math.Max(8, translationMinHeight * compression);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var extraHeight = middleHeight - minBodyHeight;
|
||||||
|
var sentenceWeight = sentenceTextDemand + 16d;
|
||||||
|
var translationWeight = translationTextDemand + 8d;
|
||||||
|
var totalWeight = Math.Max(1d, sentenceWeight + translationWeight);
|
||||||
|
sentenceHeight = sentenceMinHeight + extraHeight * (sentenceWeight / totalWeight);
|
||||||
|
translationHeight = translationMinHeight + extraHeight * (translationWeight / totalWeight);
|
||||||
|
}
|
||||||
|
|
||||||
var sentenceLayout = FitAdaptiveTextLayout(
|
var sentenceLayout = FitAdaptiveTextLayout(
|
||||||
SentenceTextBlock.Text,
|
SentenceTextBlock.Text,
|
||||||
innerWidth,
|
innerWidth,
|
||||||
sentenceHeight,
|
sentenceHeight,
|
||||||
minLines: 1,
|
minLines: sentenceMinLines,
|
||||||
maxLines: sentenceLineLimit,
|
maxLines: sentenceMaxLines,
|
||||||
minFontSize: Math.Clamp(23 * scale, 10, 42),
|
minFontSize: sentenceMinFont,
|
||||||
maxFontSize: Math.Clamp(58 * scale, 18, 80),
|
maxFontSize: Math.Clamp(58 * scale, 18, 80),
|
||||||
weightCandidates: HeadlineWeightCandidates,
|
weightCandidates: HeadlineWeightCandidates,
|
||||||
lineHeightFactor: 1.06);
|
lineHeightFactor: 1.06);
|
||||||
@@ -473,8 +534,8 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
innerWidth,
|
innerWidth,
|
||||||
translationHeight,
|
translationHeight,
|
||||||
minLines: 1,
|
minLines: 1,
|
||||||
maxLines: 2,
|
maxLines: translationMaxLines,
|
||||||
minFontSize: Math.Clamp(16 * scale, 8.5, 30),
|
minFontSize: translationMinFont,
|
||||||
maxFontSize: Math.Clamp(40 * scale, 12, 54),
|
maxFontSize: Math.Clamp(40 * scale, 12, 54),
|
||||||
weightCandidates: BodyWeightCandidates,
|
weightCandidates: BodyWeightCandidates,
|
||||||
lineHeightFactor: 1.06);
|
lineHeightFactor: 1.06);
|
||||||
@@ -483,6 +544,8 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
TranslationTextBlock.FontSize = translationLayout.FontSize;
|
TranslationTextBlock.FontSize = translationLayout.FontSize;
|
||||||
TranslationTextBlock.FontWeight = translationLayout.Weight;
|
TranslationTextBlock.FontWeight = translationLayout.Weight;
|
||||||
TranslationTextBlock.LineHeight = translationLayout.LineHeight;
|
TranslationTextBlock.LineHeight = translationLayout.LineHeight;
|
||||||
|
SentenceTextBlock.MinHeight = Math.Max(0, sentenceLayout.LineHeight * Math.Min(sentenceLayout.MaxLines, Math.Max(1, sentenceMinLines)));
|
||||||
|
TranslationTextBlock.MinHeight = Math.Max(0, translationLayout.LineHeight);
|
||||||
|
|
||||||
var sourceLayout = FitAdaptiveTextLayout(
|
var sourceLayout = FitAdaptiveTextLayout(
|
||||||
SourceTextBlock.Text,
|
SourceTextBlock.Text,
|
||||||
@@ -532,6 +595,60 @@ public partial class DailySentenceWidget : UserControl, IDesktopComponentWidget,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ApplyAutoRotateSettings()
|
||||||
|
{
|
||||||
|
var enabled = true;
|
||||||
|
var intervalMinutes = 60;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var snapshot = _settingsService.Load();
|
||||||
|
enabled = snapshot.DailySentenceAutoRotateEnabled;
|
||||||
|
intervalMinutes = NormalizeAutoRotateIntervalMinutes(snapshot.DailySentenceAutoRotateIntervalMinutes);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Keep fallback defaults.
|
||||||
|
}
|
||||||
|
|
||||||
|
_autoRotateEnabled = enabled;
|
||||||
|
_refreshTimer.Interval = TimeSpan.FromMinutes(intervalMinutes);
|
||||||
|
|
||||||
|
if (!_isAttached)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_autoRotateEnabled)
|
||||||
|
{
|
||||||
|
if (!_refreshTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_refreshTimer.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (_refreshTimer.IsEnabled)
|
||||||
|
{
|
||||||
|
_refreshTimer.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int NormalizeAutoRotateIntervalMinutes(int minutes)
|
||||||
|
{
|
||||||
|
if (minutes <= 0)
|
||||||
|
{
|
||||||
|
return 60;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (SupportedAutoRotateIntervalsMinutes.Contains(minutes))
|
||||||
|
{
|
||||||
|
return minutes;
|
||||||
|
}
|
||||||
|
|
||||||
|
return SupportedAutoRotateIntervalsMinutes
|
||||||
|
.OrderBy(value => Math.Abs(value - minutes))
|
||||||
|
.FirstOrDefault(60);
|
||||||
|
}
|
||||||
|
|
||||||
private void UpdateDateText()
|
private void UpdateDateText()
|
||||||
{
|
{
|
||||||
var now = DateTime.Now;
|
var now = DateTime.Now;
|
||||||
|
|||||||
@@ -725,9 +725,22 @@ public partial class MainWindow
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (placement.ComponentId == BuiltInComponentIds.DesktopDailySentence)
|
||||||
|
{
|
||||||
|
OpenDailySentenceComponentSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (placement.ComponentId == BuiltInComponentIds.DesktopCnrDailyNews)
|
||||||
|
{
|
||||||
|
OpenCnrDailyNewsComponentSettings();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
|
if (placement.ComponentId == BuiltInComponentIds.DesktopStudyEnvironment)
|
||||||
{
|
{
|
||||||
OpenStudyEnvironmentComponentSettings();
|
OpenStudyEnvironmentComponentSettings();
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -827,6 +840,38 @@ public partial class MainWindow
|
|||||||
ComponentSettingsWindow.Opacity = 1;
|
ComponentSettingsWindow.Opacity = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OpenDailySentenceComponentSettings()
|
||||||
|
{
|
||||||
|
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsContent = new DailySentenceSettingsWindow();
|
||||||
|
settingsContent.SettingsChanged += OnDailySentenceSettingsChanged;
|
||||||
|
ComponentSettingsContentHost.Content = settingsContent;
|
||||||
|
|
||||||
|
ComponentSettingsWindow.IsVisible = true;
|
||||||
|
ComponentSettingsWindow.Opacity = 0;
|
||||||
|
ComponentSettingsWindow.Opacity = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OpenCnrDailyNewsComponentSettings()
|
||||||
|
{
|
||||||
|
if (ComponentSettingsWindow is null || ComponentSettingsContentHost is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var settingsContent = new CnrDailyNewsSettingsWindow();
|
||||||
|
settingsContent.SettingsChanged += OnCnrDailyNewsSettingsChanged;
|
||||||
|
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)
|
||||||
@@ -931,6 +976,54 @@ public partial class MainWindow
|
|||||||
PersistSettings();
|
PersistSettings();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void OnDailySentenceSettingsChanged(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 DailySentenceWidget widget)
|
||||||
|
{
|
||||||
|
widget.RefreshFromSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistSettings();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnCnrDailyNewsSettingsChanged(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 CnrDailyNewsWidget widget)
|
||||||
|
{
|
||||||
|
widget.RefreshFromSettings();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
PersistSettings();
|
||||||
|
}
|
||||||
|
|
||||||
private void CloseComponentSettingsWindow()
|
private void CloseComponentSettingsWindow()
|
||||||
{
|
{
|
||||||
if (ComponentSettingsWindow is null)
|
if (ComponentSettingsWindow is null)
|
||||||
@@ -963,6 +1056,16 @@ public partial class MainWindow
|
|||||||
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
|
worldClockSettingsWindow.SettingsChanged -= OnWorldClockSettingsChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ComponentSettingsContentHost?.Content is DailySentenceSettingsWindow dailySentenceSettingsWindow)
|
||||||
|
{
|
||||||
|
dailySentenceSettingsWindow.SettingsChanged -= OnDailySentenceSettingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ComponentSettingsContentHost?.Content is CnrDailyNewsSettingsWindow cnrDailyNewsSettingsWindow)
|
||||||
|
{
|
||||||
|
cnrDailyNewsSettingsWindow.SettingsChanged -= OnCnrDailyNewsSettingsChanged;
|
||||||
|
}
|
||||||
|
|
||||||
ComponentSettingsWindow.Opacity = 0;
|
ComponentSettingsWindow.Opacity = 0;
|
||||||
|
|
||||||
DispatcherTimer.RunOnce(() =>
|
DispatcherTimer.RunOnce(() =>
|
||||||
@@ -1374,6 +1477,14 @@ public partial class MainWindow
|
|||||||
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(componentId, BuiltInComponentIds.DesktopCnrDailyNews, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
// Keep CNR widget at a 2:1 ratio: 4x2, 6x3, 8x4...
|
||||||
|
return SnapSpanToScaleRules(
|
||||||
|
span,
|
||||||
|
new ComponentScaleRule(WidthUnit: 2, HeightUnit: 1, MinScale: 2));
|
||||||
|
}
|
||||||
|
|
||||||
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyNoiseCurve, StringComparison.OrdinalIgnoreCase))
|
if (string.Equals(componentId, BuiltInComponentIds.DesktopStudyNoiseCurve, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
// Keep noise curve widget in a 2:1 ratio with minimum 4x2.
|
// Keep noise curve widget in a 2:1 ratio with minimum 4x2.
|
||||||
|
|||||||
Reference in New Issue
Block a user