mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-20 23:54:26 +08:00
0.4.7
2×2英语单词组件,修复了stcn组件
This commit is contained in:
@@ -30,6 +30,7 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopDailyPoetry = "DesktopDailyPoetry";
|
||||
public const string DesktopDailyArtwork = "DesktopDailyArtwork";
|
||||
public const string DesktopDailyWord = "DesktopDailyWord";
|
||||
public const string DesktopDailyWord2x2 = "DesktopDailyWord2x2";
|
||||
public const string DesktopCnrDailyNews = "DesktopCnrDailyNews";
|
||||
public const string DesktopBilibiliHotSearch = "DesktopBilibiliHotSearch";
|
||||
public const string DesktopStcn24Forum = "DesktopStcn24Forum";
|
||||
|
||||
@@ -234,6 +234,15 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopDailyWord2x2,
|
||||
"Daily Word 2x2",
|
||||
"Book",
|
||||
"Info",
|
||||
MinWidthCells: 2,
|
||||
MinHeightCells: 2,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopCnrDailyNews,
|
||||
"CNR Daily News",
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
"component.daily_poetry": "Daily Poetry",
|
||||
"component.daily_artwork": "Daily Artwork",
|
||||
"component.daily_word": "Daily Word",
|
||||
"component.daily_word_2x2": "Daily Word 2x2",
|
||||
"component.cnr_daily_news": "CNR Headlines",
|
||||
"component.bilibili_hot_search": "Bilibili Hot Search",
|
||||
"component.stcn24_forum": "STCN 24",
|
||||
@@ -343,6 +344,7 @@
|
||||
"dailyword.widget.fallback_meaning": "Youdao dictionary is temporarily unavailable.",
|
||||
"dailyword.widget.fallback_example": "Tap the refresh button and try again.",
|
||||
"dailyword.widget.fallback_example_translation": "It will retry when network recovers.",
|
||||
"dailyword2x2.widget.tap_to_show": "Tap to reveal meaning",
|
||||
"cnrnews.widget.loading": "Loading...",
|
||||
"cnrnews.widget.loading_title": "Fetching CNR headlines",
|
||||
"cnrnews.widget.loading_subtitle": "Please wait",
|
||||
|
||||
@@ -295,6 +295,7 @@
|
||||
"component.daily_poetry": "每日诗词",
|
||||
"component.daily_artwork": "每日名画",
|
||||
"component.daily_word": "每日单词",
|
||||
"component.daily_word_2x2": "每日单词 2x2",
|
||||
"component.cnr_daily_news": "央广网头条",
|
||||
"component.bilibili_hot_search": "B站热搜",
|
||||
"component.stcn24_forum": "STCN 24",
|
||||
@@ -343,6 +344,7 @@
|
||||
"dailyword.widget.fallback_meaning": "有道词典暂不可用",
|
||||
"dailyword.widget.fallback_example": "请点击右上角刷新重试",
|
||||
"dailyword.widget.fallback_example_translation": "网络恢复后将自动更新",
|
||||
"dailyword2x2.widget.tap_to_show": "点击查看释义",
|
||||
"cnrnews.widget.loading": "加载中...",
|
||||
"cnrnews.widget.loading_title": "正在获取新闻热点",
|
||||
"cnrnews.widget.loading_subtitle": "请稍候",
|
||||
|
||||
@@ -46,6 +46,8 @@ public sealed class LocalizationService
|
||||
if (File.Exists(filePath))
|
||||
{
|
||||
var json = File.ReadAllText(filePath);
|
||||
// Defensive: tolerate accidentally duplicated UTF-8 BOM characters at file start.
|
||||
json = json.TrimStart('\uFEFF');
|
||||
var data = JsonSerializer.Deserialize<Dictionary<string, string>>(json, JsonOptions);
|
||||
if (data is not null)
|
||||
{
|
||||
@@ -62,4 +64,3 @@ public sealed class LocalizationService
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -819,15 +819,25 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
string sourceType,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
var normalizedSourceType = Stcn24ForumSourceTypes.Normalize(sourceType);
|
||||
var isLatestCreatedSource = string.Equals(
|
||||
normalizedSourceType,
|
||||
Stcn24ForumSourceTypes.LatestCreated,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
var safeCount = Math.Clamp(targetCount, 1, 12);
|
||||
var requestCount = Math.Clamp(Math.Max(safeCount * 3, 12), safeCount, 40);
|
||||
var keyword = NormalizeInlineText(_options.SmartTeachStcnKeyword);
|
||||
if (string.IsNullOrWhiteSpace(keyword))
|
||||
if (isLatestCreatedSource)
|
||||
{
|
||||
// For latest posts, rely on discussion id ordering from the full discussion stream.
|
||||
keyword = string.Empty;
|
||||
}
|
||||
else if (string.IsNullOrWhiteSpace(keyword))
|
||||
{
|
||||
keyword = "STCN";
|
||||
}
|
||||
|
||||
var sortToken = ResolveSmartTeachDiscussionSortToken(sourceType);
|
||||
var sortToken = ResolveSmartTeachDiscussionSortToken(normalizedSourceType);
|
||||
|
||||
var requestUrl = string.Format(
|
||||
CultureInfo.InvariantCulture,
|
||||
@@ -895,10 +905,17 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
}
|
||||
}
|
||||
|
||||
var items = new List<Stcn24ForumPostItemSnapshot>(safeCount);
|
||||
var candidates = new List<(Stcn24ForumPostItemSnapshot Item, long? DiscussionId)>(requestCount);
|
||||
foreach (var discussionNode in dataArray.EnumerateArray())
|
||||
{
|
||||
if (discussionNode.ValueKind != JsonValueKind.Object || IsSmartTeachPinnedDiscussion(discussionNode))
|
||||
if (discussionNode.ValueKind != JsonValueKind.Object)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var discussionType = ReadString(discussionNode, "type");
|
||||
if (!string.Equals(discussionType, "discussions", StringComparison.OrdinalIgnoreCase) ||
|
||||
IsSmartTeachPinnedDiscussion(discussionNode))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@@ -940,22 +957,37 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
var createdAtText = ReadString(discussionNode, "attributes", "createdAt");
|
||||
var createdAt = TryParseDateTimeOffset(createdAtText);
|
||||
|
||||
items.Add(new Stcn24ForumPostItemSnapshot(
|
||||
candidates.Add((
|
||||
new Stcn24ForumPostItemSnapshot(
|
||||
Title: title,
|
||||
Url: targetUrl,
|
||||
AuthorDisplayName: authorDisplayName,
|
||||
AuthorAvatarUrl: authorAvatarUrl,
|
||||
CreatedAt: createdAt));
|
||||
CreatedAt: createdAt),
|
||||
TryParseSmartTeachDiscussionId(discussionId)));
|
||||
}
|
||||
|
||||
if (items.Count >= safeCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
IReadOnlyList<Stcn24ForumPostItemSnapshot> items;
|
||||
if (isLatestCreatedSource)
|
||||
{
|
||||
items = candidates
|
||||
.OrderByDescending(candidate => candidate.DiscussionId ?? long.MinValue)
|
||||
.ThenByDescending(candidate => candidate.Item.CreatedAt ?? DateTimeOffset.MinValue)
|
||||
.Take(safeCount)
|
||||
.Select(candidate => candidate.Item)
|
||||
.ToArray();
|
||||
}
|
||||
else
|
||||
{
|
||||
items = candidates
|
||||
.Take(safeCount)
|
||||
.Select(candidate => candidate.Item)
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
return new Stcn24ForumPostsSnapshot(
|
||||
Provider: "SmartTeachForum",
|
||||
Source: ResolveStcn24ForumSourceLabel(sourceType),
|
||||
Source: ResolveStcn24ForumSourceLabel(normalizedSourceType),
|
||||
Items: items,
|
||||
FetchedAt: DateTimeOffset.UtcNow);
|
||||
}
|
||||
@@ -2331,6 +2363,18 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
: null;
|
||||
}
|
||||
|
||||
private static long? TryParseSmartTeachDiscussionId(string? rawValue)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(rawValue))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return long.TryParse(rawValue.Trim(), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value)
|
||||
? value
|
||||
: null;
|
||||
}
|
||||
|
||||
private static string NormalizeInlineText(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
@@ -2566,4 +2610,3 @@ public sealed class RecommendationDataService : IRecommendationInfoService, IDis
|
||||
: $"{text[..maxLength]}...";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
89
LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml
Normal file
89
LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml
Normal file
@@ -0,0 +1,89 @@
|
||||
<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"
|
||||
xmlns:fi="using:FluentIcons.Avalonia"
|
||||
mc:Ignorable="d"
|
||||
d:DesignWidth="220"
|
||||
d:DesignHeight="220"
|
||||
x:Class="LanMountainDesktop.Views.Components.DailyWord2x2Widget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="30"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True"
|
||||
BorderThickness="0"
|
||||
Padding="0">
|
||||
<Grid>
|
||||
<Border x:Name="CardBorder"
|
||||
Background="#FCFBFA"
|
||||
CornerRadius="30"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="12,11,12,11"
|
||||
PointerPressed="OnCardPointerPressed">
|
||||
<Grid RowDefinitions="Auto,*"
|
||||
RowSpacing="8">
|
||||
<Grid ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="6">
|
||||
<TextBlock x:Name="WordTextBlock"
|
||||
Text="design"
|
||||
Foreground="#2B2F35"
|
||||
FontSize="38"
|
||||
FontWeight="Bold"
|
||||
MaxLines="1"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.Column="1"
|
||||
Width="30"
|
||||
Height="30"
|
||||
CornerRadius="15"
|
||||
Background="#EEF1F4"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="0"
|
||||
Focusable="False"
|
||||
Click="OnRefreshButtonClick">
|
||||
<fi:SymbolIcon x:Name="RefreshIcon"
|
||||
Symbol="ArrowClockwise"
|
||||
IconVariant="Regular"
|
||||
FontSize="14"
|
||||
Foreground="#5E6671" />
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<Grid Grid.Row="1">
|
||||
<TextBlock x:Name="MeaningTextBlock"
|
||||
Text="n. design; plan; layout"
|
||||
Foreground="#5A6069"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="5"
|
||||
IsVisible="False" />
|
||||
|
||||
<TextBlock x:Name="HiddenHintTextBlock"
|
||||
Text="Tap to reveal meaning"
|
||||
Foreground="#8A9099"
|
||||
FontSize="18"
|
||||
FontWeight="SemiBold"
|
||||
TextWrapping="Wrap"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
MaxLines="4" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
IsVisible="False"
|
||||
Text="Loading..."
|
||||
Foreground="#6A6F77"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
507
LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
Normal file
507
LanMountainDesktop/Views/Components/DailyWord2x2Widget.axaml.cs
Normal file
@@ -0,0 +1,507 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.VisualTree;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class DailyWord2x2Widget : UserControl, IDesktopComponentWidget, IRecommendationInfoAwareComponentWidget
|
||||
{
|
||||
private static readonly Regex MultiWhitespaceRegex = new(@"\s+", RegexOptions.Compiled);
|
||||
private static readonly FontFamily MiSansFontFamily = new("MiSans VF, avares://LanMountainDesktop/Assets/Fonts#MiSans");
|
||||
private static readonly IRecommendationInfoService DefaultRecommendationService = new RecommendationDataService();
|
||||
|
||||
private const double BaseCellSize = 48d;
|
||||
private const int BaseWidthCells = 2;
|
||||
private const int BaseHeightCells = 2;
|
||||
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
{
|
||||
Interval = TimeSpan.FromHours(6)
|
||||
};
|
||||
|
||||
private readonly AppSettingsService _appSettingsService = new();
|
||||
private readonly ComponentSettingsService _componentSettingsService = new();
|
||||
private readonly LocalizationService _localizationService = new();
|
||||
|
||||
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
|
||||
private CancellationTokenSource? _refreshCts;
|
||||
private DailyWordSnapshot? _latestSnapshot;
|
||||
private string _languageCode = "zh-CN";
|
||||
private double _currentCellSize = BaseCellSize;
|
||||
private bool _isAttached;
|
||||
private bool _isRefreshing;
|
||||
private bool _autoRefreshEnabled = true;
|
||||
private bool _isMeaningVisible;
|
||||
|
||||
public DailyWord2x2Widget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
WordTextBlock.FontFamily = MiSansFontFamily;
|
||||
MeaningTextBlock.FontFamily = MiSansFontFamily;
|
||||
HiddenHintTextBlock.FontFamily = MiSansFontFamily;
|
||||
StatusTextBlock.FontFamily = MiSansFontFamily;
|
||||
|
||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
UpdateLanguageCode();
|
||||
ApplyAutoRefreshSettings();
|
||||
ApplyLoadingState();
|
||||
UpdateRefreshButtonState();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
public void SetRecommendationInfoService(IRecommendationInfoService recommendationInfoService)
|
||||
{
|
||||
_recommendationService = recommendationInfoService ?? DefaultRecommendationService;
|
||||
if (_isAttached)
|
||||
{
|
||||
_ = RefreshWordAsync(forceRefresh: false);
|
||||
}
|
||||
}
|
||||
|
||||
public void RefreshFromSettings()
|
||||
{
|
||||
_recommendationService.ClearCache();
|
||||
ApplyAutoRefreshSettings();
|
||||
if (_isAttached)
|
||||
{
|
||||
_ = RefreshWordAsync(forceRefresh: true);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
ApplyAutoRefreshSettings();
|
||||
UpdateRefreshButtonState();
|
||||
_ = RefreshWordAsync(forceRefresh: false);
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = false;
|
||||
_refreshTimer.Stop();
|
||||
CancelRefreshRequest();
|
||||
UpdateRefreshButtonState();
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (_isRefreshing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await RefreshWordAsync(forceRefresh: true);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private async void OnRefreshTimerTick(object? sender, EventArgs e)
|
||||
{
|
||||
await RefreshWordAsync(forceRefresh: false);
|
||||
}
|
||||
|
||||
private void OnCardPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (_latestSnapshot is null || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.Source is Visual sourceVisual)
|
||||
{
|
||||
for (Visual? current = sourceVisual; current is not null; current = current.GetVisualParent())
|
||||
{
|
||||
if (ReferenceEquals(current, RefreshButton))
|
||||
{
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_isMeaningVisible = !_isMeaningVisible;
|
||||
UpdateRevealState();
|
||||
UpdateAdaptiveLayout();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private async Task RefreshWordAsync(bool forceRefresh)
|
||||
{
|
||||
if (!_isAttached || _isRefreshing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isRefreshing = true;
|
||||
UpdateRefreshButtonState();
|
||||
UpdateLanguageCode();
|
||||
|
||||
var cts = new CancellationTokenSource();
|
||||
var previous = Interlocked.Exchange(ref _refreshCts, cts);
|
||||
previous?.Cancel();
|
||||
previous?.Dispose();
|
||||
|
||||
try
|
||||
{
|
||||
var query = new DailyWordQuery(
|
||||
Locale: _languageCode,
|
||||
ForceRefresh: forceRefresh);
|
||||
var result = await _recommendationService.GetDailyWordAsync(query, cts.Token);
|
||||
if (!_isAttached || cts.IsCancellationRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!result.Success || result.Data is null)
|
||||
{
|
||||
ApplyFailedState();
|
||||
return;
|
||||
}
|
||||
|
||||
ApplySnapshot(result.Data);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// Ignore canceled requests.
|
||||
}
|
||||
catch
|
||||
{
|
||||
if (_isAttached && !cts.IsCancellationRequested)
|
||||
{
|
||||
ApplyFailedState();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (ReferenceEquals(_refreshCts, cts))
|
||||
{
|
||||
_refreshCts = null;
|
||||
}
|
||||
|
||||
cts.Dispose();
|
||||
_isRefreshing = false;
|
||||
UpdateRefreshButtonState();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySnapshot(DailyWordSnapshot snapshot)
|
||||
{
|
||||
_latestSnapshot = snapshot;
|
||||
WordTextBlock.Text = NormalizeCompactText(snapshot.Word);
|
||||
MeaningTextBlock.Text = BuildMeaningPreview(snapshot.Meaning);
|
||||
HiddenHintTextBlock.Text = L("dailyword2x2.widget.tap_to_show", "Tap to reveal meaning");
|
||||
StatusTextBlock.IsVisible = false;
|
||||
|
||||
UpdateRevealState();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void ApplyLoadingState()
|
||||
{
|
||||
_latestSnapshot = null;
|
||||
_isMeaningVisible = false;
|
||||
WordTextBlock.Text = L("dailyword.widget.loading_word", "daily word");
|
||||
MeaningTextBlock.Text = L("dailyword.widget.loading_meaning", "Fetching meaning...");
|
||||
HiddenHintTextBlock.Text = L("dailyword.widget.loading", "Loading...");
|
||||
StatusTextBlock.Text = L("dailyword.widget.loading", "Loading...");
|
||||
StatusTextBlock.IsVisible = true;
|
||||
UpdateRevealState();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void ApplyFailedState()
|
||||
{
|
||||
_latestSnapshot = null;
|
||||
_isMeaningVisible = false;
|
||||
WordTextBlock.Text = L("dailyword.widget.fallback_word", "daily word");
|
||||
MeaningTextBlock.Text = L("dailyword.widget.fallback_meaning", "Youdao dictionary is temporarily unavailable.");
|
||||
HiddenHintTextBlock.Text = L("dailyword.widget.fetch_failed", "Daily word fetch failed");
|
||||
StatusTextBlock.Text = L("dailyword.widget.fetch_failed", "Daily word fetch failed");
|
||||
StatusTextBlock.IsVisible = true;
|
||||
UpdateRevealState();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void UpdateRevealState()
|
||||
{
|
||||
var canShowMeaning = _latestSnapshot is not null && !string.IsNullOrWhiteSpace(MeaningTextBlock.Text);
|
||||
var showMeaning = _isMeaningVisible && canShowMeaning;
|
||||
MeaningTextBlock.IsVisible = showMeaning;
|
||||
HiddenHintTextBlock.IsVisible = !showMeaning;
|
||||
|
||||
if (!showMeaning && _latestSnapshot is not null)
|
||||
{
|
||||
HiddenHintTextBlock.Text = L("dailyword2x2.widget.tap_to_show", "Tap to reveal meaning");
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateAdaptiveLayout()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
RootBorder.CornerRadius = new CornerRadius(Math.Clamp(30 * scale, 14, 40));
|
||||
CardBorder.CornerRadius = RootBorder.CornerRadius;
|
||||
CardBorder.Padding = new Thickness(
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(11 * scale, 7, 16),
|
||||
Math.Clamp(12 * scale, 8, 18),
|
||||
Math.Clamp(11 * scale, 7, 16));
|
||||
|
||||
var refreshSize = Math.Clamp(30 * scale, 20, 38);
|
||||
RefreshButton.Width = refreshSize;
|
||||
RefreshButton.Height = refreshSize;
|
||||
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
|
||||
RefreshIcon.FontSize = Math.Clamp(14 * scale, 10, 20);
|
||||
|
||||
var contentWidth = Math.Max(80, totalWidth - CardBorder.Padding.Left - CardBorder.Padding.Right);
|
||||
var wordWidth = Math.Max(48, contentWidth - refreshSize - Math.Clamp(6 * scale, 4, 10));
|
||||
WordTextBlock.MaxWidth = wordWidth;
|
||||
|
||||
var contentHeight = Math.Max(52, totalHeight - CardBorder.Padding.Top - CardBorder.Padding.Bottom);
|
||||
var wordHeightBudget = Math.Max(18, contentHeight * 0.34);
|
||||
var detailHeightBudget = Math.Max(18, contentHeight - wordHeightBudget - Math.Clamp(8 * scale, 4, 14));
|
||||
|
||||
WordTextBlock.FontSize = FitFontSize(
|
||||
WordTextBlock.Text,
|
||||
wordWidth,
|
||||
wordHeightBudget,
|
||||
maxLines: 1,
|
||||
minFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||
maxFontSize: Math.Clamp(38 * scale, 20, 50),
|
||||
weight: FontWeight.Bold,
|
||||
lineHeightFactor: 1.02);
|
||||
WordTextBlock.LineHeight = WordTextBlock.FontSize * 1.02;
|
||||
|
||||
var detailFont = FitFontSize(
|
||||
MeaningTextBlock.IsVisible ? MeaningTextBlock.Text : HiddenHintTextBlock.Text,
|
||||
contentWidth,
|
||||
detailHeightBudget,
|
||||
maxLines: MeaningTextBlock.IsVisible ? 5 : 4,
|
||||
minFontSize: Math.Clamp(12 * scale, 9, 14),
|
||||
maxFontSize: Math.Clamp(18 * scale, 12, 22),
|
||||
weight: FontWeight.SemiBold,
|
||||
lineHeightFactor: 1.10);
|
||||
|
||||
MeaningTextBlock.MaxWidth = contentWidth;
|
||||
MeaningTextBlock.FontSize = detailFont;
|
||||
MeaningTextBlock.LineHeight = detailFont * 1.10;
|
||||
MeaningTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 4 : 5;
|
||||
|
||||
HiddenHintTextBlock.MaxWidth = contentWidth;
|
||||
HiddenHintTextBlock.FontSize = detailFont;
|
||||
HiddenHintTextBlock.LineHeight = detailFont * 1.10;
|
||||
HiddenHintTextBlock.MaxLines = totalHeight < _currentCellSize * 1.8 ? 3 : 4;
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(14 * scale, 9, 18);
|
||||
}
|
||||
|
||||
private void UpdateRefreshButtonState()
|
||||
{
|
||||
RefreshButton.IsEnabled = !_isRefreshing;
|
||||
RefreshButton.Opacity = _isRefreshing ? 0.60 : 1.0;
|
||||
RefreshIcon.Opacity = _isRefreshing ? 0.60 : 1.0;
|
||||
}
|
||||
|
||||
private void UpdateLanguageCode()
|
||||
{
|
||||
try
|
||||
{
|
||||
var snapshot = _appSettingsService.Load();
|
||||
_languageCode = _localizationService.NormalizeLanguageCode(snapshot.LanguageCode);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_languageCode = "zh-CN";
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyAutoRefreshSettings()
|
||||
{
|
||||
var enabled = true;
|
||||
var intervalMinutes = 360;
|
||||
|
||||
try
|
||||
{
|
||||
var snapshot = _componentSettingsService.Load();
|
||||
enabled = snapshot.DailyWordAutoRefreshEnabled;
|
||||
intervalMinutes = NormalizeAutoRefreshIntervalMinutes(snapshot.DailyWordAutoRefreshIntervalMinutes);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Keep fallback defaults.
|
||||
}
|
||||
|
||||
_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 360;
|
||||
}
|
||||
|
||||
if (SupportedAutoRefreshIntervalsMinutes.Contains(minutes))
|
||||
{
|
||||
return minutes;
|
||||
}
|
||||
|
||||
return SupportedAutoRefreshIntervalsMinutes
|
||||
.OrderBy(value => Math.Abs(value - minutes))
|
||||
.FirstOrDefault(360);
|
||||
}
|
||||
|
||||
private void CancelRefreshRequest()
|
||||
{
|
||||
var cts = Interlocked.Exchange(ref _refreshCts, null);
|
||||
if (cts is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
cts.Cancel();
|
||||
cts.Dispose();
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var cellScale = Math.Clamp(_currentCellSize / BaseCellSize, 0.56, 2.0);
|
||||
var widthScale = Bounds.Width > 1
|
||||
? Math.Clamp(Bounds.Width / Math.Max(1, _currentCellSize * BaseWidthCells), 0.56, 2.0)
|
||||
: 1;
|
||||
var heightScale = Bounds.Height > 1
|
||||
? Math.Clamp(Bounds.Height / Math.Max(1, _currentCellSize * BaseHeightCells), 0.56, 2.0)
|
||||
: 1;
|
||||
return Math.Clamp(Math.Min(cellScale, Math.Min(widthScale, heightScale)), 0.56, 2.0);
|
||||
}
|
||||
|
||||
private string L(string key, string fallback)
|
||||
{
|
||||
return _localizationService.GetString(_languageCode, key, fallback);
|
||||
}
|
||||
|
||||
private static string BuildMeaningPreview(string? rawMeaning)
|
||||
{
|
||||
var normalized = NormalizeCompactText(rawMeaning);
|
||||
if (string.IsNullOrWhiteSpace(normalized))
|
||||
{
|
||||
return "Meaning unavailable";
|
||||
}
|
||||
|
||||
var compact = normalized.Replace(";", "; ", StringComparison.Ordinal);
|
||||
return compact.Length <= 160 ? compact : $"{compact[..160]}...";
|
||||
}
|
||||
|
||||
private static string NormalizeCompactText(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
|
||||
}
|
||||
|
||||
private static double FitFontSize(
|
||||
string? text,
|
||||
double maxWidth,
|
||||
double maxHeight,
|
||||
int maxLines,
|
||||
double minFontSize,
|
||||
double maxFontSize,
|
||||
FontWeight weight,
|
||||
double lineHeightFactor)
|
||||
{
|
||||
var content = string.IsNullOrWhiteSpace(text) ? " " : text.Trim();
|
||||
var min = Math.Max(6, minFontSize);
|
||||
var max = Math.Max(min, maxFontSize);
|
||||
var low = min;
|
||||
var high = max;
|
||||
var best = min;
|
||||
|
||||
for (var i = 0; i < 18; i++)
|
||||
{
|
||||
var candidate = (low + high) / 2d;
|
||||
var lineHeight = candidate * lineHeightFactor;
|
||||
var size = MeasureTextSize(content, candidate, weight, Math.Max(1, maxWidth), lineHeight);
|
||||
var lineCount = Math.Max(1, (int)Math.Ceiling(size.Height / Math.Max(1, lineHeight)));
|
||||
var fits = size.Height <= maxHeight + 0.6 && lineCount <= Math.Max(1, maxLines);
|
||||
|
||||
if (fits)
|
||||
{
|
||||
best = candidate;
|
||||
low = candidate;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = candidate;
|
||||
}
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
private static Size MeasureTextSize(string text, double fontSize, FontWeight weight, double maxWidth, double lineHeight)
|
||||
{
|
||||
var probe = new TextBlock
|
||||
{
|
||||
Text = text,
|
||||
FontFamily = MiSansFontFamily,
|
||||
FontSize = fontSize,
|
||||
FontWeight = weight,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
LineHeight = lineHeight
|
||||
};
|
||||
|
||||
probe.Measure(new Size(Math.Max(1, maxWidth), double.PositiveInfinity));
|
||||
return probe.DesiredSize;
|
||||
}
|
||||
}
|
||||
@@ -240,6 +240,11 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
"component.daily_word",
|
||||
() => new DailyWordWidget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 14, 30)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopDailyWord2x2,
|
||||
"component.daily_word_2x2",
|
||||
() => new DailyWord2x2Widget(),
|
||||
cellSize => Math.Clamp(cellSize * 0.34, 12, 26)),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopCnrDailyNews,
|
||||
"component.cnr_daily_news",
|
||||
|
||||
@@ -22,7 +22,7 @@
|
||||
BorderThickness="0"
|
||||
Padding="12,12,12,12">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto"
|
||||
RowDefinitions="Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto,Auto"
|
||||
RowSpacing="6">
|
||||
<Grid x:Name="HeaderGrid"
|
||||
Grid.Row="0"
|
||||
@@ -231,6 +231,170 @@
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="PostItem5Host"
|
||||
Grid.Row="5"
|
||||
Tag="4"
|
||||
Background="#F7F8FA"
|
||||
CornerRadius="10"
|
||||
Padding="8,6"
|
||||
PointerPressed="OnPostItemPointerPressed">
|
||||
<Grid x:Name="PostItem5Grid"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
<Border x:Name="PostItem5AvatarHost"
|
||||
Width="30"
|
||||
Height="30"
|
||||
CornerRadius="15"
|
||||
Background="#E7EBF4"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="PostItem5AvatarImage"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock x:Name="PostItem5AvatarFallbackText"
|
||||
Text="?"
|
||||
Foreground="#4A5466"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PostItem5TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Loading..."
|
||||
Foreground="#202327"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
MaxLines="1"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="PostItem6Host"
|
||||
Grid.Row="6"
|
||||
Tag="5"
|
||||
Background="#F7F8FA"
|
||||
CornerRadius="10"
|
||||
Padding="8,6"
|
||||
PointerPressed="OnPostItemPointerPressed">
|
||||
<Grid x:Name="PostItem6Grid"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
<Border x:Name="PostItem6AvatarHost"
|
||||
Width="30"
|
||||
Height="30"
|
||||
CornerRadius="15"
|
||||
Background="#E7EBF4"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="PostItem6AvatarImage"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock x:Name="PostItem6AvatarFallbackText"
|
||||
Text="?"
|
||||
Foreground="#4A5466"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PostItem6TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Loading..."
|
||||
Foreground="#202327"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
MaxLines="1"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="PostItem7Host"
|
||||
Grid.Row="7"
|
||||
Tag="6"
|
||||
Background="#F7F8FA"
|
||||
CornerRadius="10"
|
||||
Padding="8,6"
|
||||
PointerPressed="OnPostItemPointerPressed">
|
||||
<Grid x:Name="PostItem7Grid"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
<Border x:Name="PostItem7AvatarHost"
|
||||
Width="30"
|
||||
Height="30"
|
||||
CornerRadius="15"
|
||||
Background="#E7EBF4"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="PostItem7AvatarImage"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock x:Name="PostItem7AvatarFallbackText"
|
||||
Text="?"
|
||||
Foreground="#4A5466"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PostItem7TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Loading..."
|
||||
Foreground="#202327"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
MaxLines="1"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<Border x:Name="PostItem8Host"
|
||||
Grid.Row="8"
|
||||
Tag="7"
|
||||
Background="#F7F8FA"
|
||||
CornerRadius="10"
|
||||
Padding="8,6"
|
||||
PointerPressed="OnPostItemPointerPressed">
|
||||
<Grid x:Name="PostItem8Grid"
|
||||
ColumnDefinitions="Auto,*"
|
||||
ColumnSpacing="8">
|
||||
<Border x:Name="PostItem8AvatarHost"
|
||||
Width="30"
|
||||
Height="30"
|
||||
CornerRadius="15"
|
||||
Background="#E7EBF4"
|
||||
ClipToBounds="True">
|
||||
<Grid>
|
||||
<Image x:Name="PostItem8AvatarImage"
|
||||
Stretch="UniformToFill" />
|
||||
<TextBlock x:Name="PostItem8AvatarFallbackText"
|
||||
Text="?"
|
||||
Foreground="#4A5466"
|
||||
FontSize="13"
|
||||
FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="PostItem8TitleTextBlock"
|
||||
Grid.Column="1"
|
||||
Text="Loading..."
|
||||
Foreground="#202327"
|
||||
FontSize="14"
|
||||
FontWeight="SemiBold"
|
||||
MaxLines="1"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -35,7 +35,8 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
private const double BaseCellSize = 48d;
|
||||
private const int BaseWidthCells = 4;
|
||||
private const int BaseHeightCells = 4;
|
||||
private const int MaxDisplayItemCount = 4;
|
||||
private const int BaseDisplayItemCount = 4;
|
||||
private const int MaxDisplayItemCount = 8;
|
||||
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
|
||||
|
||||
private readonly DispatcherTimer _refreshTimer = new()
|
||||
@@ -55,6 +56,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
private string _languageCode = "zh-CN";
|
||||
private string _sourceType = Stcn24ForumSourceTypes.LatestCreated;
|
||||
private double _currentCellSize = BaseCellSize;
|
||||
private int _visibleItemCount = BaseDisplayItemCount;
|
||||
private bool _isAttached;
|
||||
private bool _isRefreshing;
|
||||
private bool _autoRefreshEnabled = true;
|
||||
@@ -76,10 +78,18 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
PostItem2TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem3TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem4TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem5TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem6TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem7TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem8TitleTextBlock.FontFamily = MiSansFontFamily;
|
||||
PostItem1AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem2AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem3AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem4AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem5AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem6AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem7AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
PostItem8AvatarFallbackText.FontFamily = MiSansFontFamily;
|
||||
StatusTextBlock.FontFamily = MiSansFontFamily;
|
||||
|
||||
_itemVisuals.Add(new ForumItemVisual(
|
||||
@@ -110,6 +120,34 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
PostItem4AvatarImage,
|
||||
PostItem4AvatarFallbackText,
|
||||
PostItem4TitleTextBlock));
|
||||
_itemVisuals.Add(new ForumItemVisual(
|
||||
PostItem5Host,
|
||||
PostItem5Grid,
|
||||
PostItem5AvatarHost,
|
||||
PostItem5AvatarImage,
|
||||
PostItem5AvatarFallbackText,
|
||||
PostItem5TitleTextBlock));
|
||||
_itemVisuals.Add(new ForumItemVisual(
|
||||
PostItem6Host,
|
||||
PostItem6Grid,
|
||||
PostItem6AvatarHost,
|
||||
PostItem6AvatarImage,
|
||||
PostItem6AvatarFallbackText,
|
||||
PostItem6TitleTextBlock));
|
||||
_itemVisuals.Add(new ForumItemVisual(
|
||||
PostItem7Host,
|
||||
PostItem7Grid,
|
||||
PostItem7AvatarHost,
|
||||
PostItem7AvatarImage,
|
||||
PostItem7AvatarFallbackText,
|
||||
PostItem7TitleTextBlock));
|
||||
_itemVisuals.Add(new ForumItemVisual(
|
||||
PostItem8Host,
|
||||
PostItem8Grid,
|
||||
PostItem8AvatarHost,
|
||||
PostItem8AvatarImage,
|
||||
PostItem8AvatarFallbackText,
|
||||
PostItem8TitleTextBlock));
|
||||
|
||||
_refreshTimer.Tick += OnRefreshTimerTick;
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
@@ -222,7 +260,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
{
|
||||
var query = new Stcn24ForumPostsQuery(
|
||||
Locale: _languageCode,
|
||||
ItemCount: MaxDisplayItemCount,
|
||||
ItemCount: _visibleItemCount,
|
||||
SourceType: _sourceType,
|
||||
ForceRefresh: forceRefresh);
|
||||
var result = await _recommendationService.GetStcn24ForumPostsAsync(query, cts.Token);
|
||||
@@ -274,7 +312,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
}
|
||||
|
||||
_activeItems.Add(item);
|
||||
if (_activeItems.Count >= MaxDisplayItemCount)
|
||||
if (_activeItems.Count >= _visibleItemCount)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -284,6 +322,14 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
for (var i = 0; i < _itemVisuals.Count; i++)
|
||||
{
|
||||
var visual = _itemVisuals[i];
|
||||
var isRowVisible = i < _visibleItemCount;
|
||||
visual.Host.IsVisible = isRowVisible;
|
||||
if (!isRowVisible)
|
||||
{
|
||||
SetAvatarBitmap(i, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (i < _activeItems.Count)
|
||||
{
|
||||
var item = _activeItems[i];
|
||||
@@ -304,6 +350,7 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
UpdateAdaptiveLayout();
|
||||
|
||||
var tasks = _activeItems
|
||||
.Take(_visibleItemCount)
|
||||
.Select(item => TryDownloadAvatarBitmapAsync(item.AuthorAvatarUrl, cancellationToken))
|
||||
.ToArray();
|
||||
if (tasks.Length == 0)
|
||||
@@ -338,6 +385,14 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
for (var i = 0; i < _itemVisuals.Count; i++)
|
||||
{
|
||||
var visual = _itemVisuals[i];
|
||||
var isRowVisible = i < _visibleItemCount;
|
||||
visual.Host.IsVisible = isRowVisible;
|
||||
if (!isRowVisible)
|
||||
{
|
||||
SetAvatarBitmap(i, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
visual.TitleTextBlock.Text = loadingText;
|
||||
visual.AvatarFallbackText.Text = "?";
|
||||
SetAvatarBitmap(i, null);
|
||||
@@ -357,6 +412,14 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
for (var i = 0; i < _itemVisuals.Count; i++)
|
||||
{
|
||||
var visual = _itemVisuals[i];
|
||||
var isRowVisible = i < _visibleItemCount;
|
||||
visual.Host.IsVisible = isRowVisible;
|
||||
if (!isRowVisible)
|
||||
{
|
||||
SetAvatarBitmap(i, null);
|
||||
continue;
|
||||
}
|
||||
|
||||
visual.TitleTextBlock.Text = fallbackText;
|
||||
visual.AvatarFallbackText.Text = "?";
|
||||
SetAvatarBitmap(i, null);
|
||||
@@ -374,7 +437,11 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
for (var i = 0; i < _itemVisuals.Count; i++)
|
||||
{
|
||||
var visual = _itemVisuals[i];
|
||||
var enabled = i < _activeItems.Count && !string.IsNullOrWhiteSpace(_activeItems[i].Url);
|
||||
var inVisibleRange = i < _visibleItemCount;
|
||||
visual.Host.IsVisible = inVisibleRange;
|
||||
var enabled = inVisibleRange &&
|
||||
i < _activeItems.Count &&
|
||||
!string.IsNullOrWhiteSpace(_activeItems[i].Url);
|
||||
visual.Host.IsHitTestVisible = enabled;
|
||||
visual.Host.Opacity = enabled ? 1.0 : 0.72;
|
||||
visual.Host.Cursor = enabled
|
||||
@@ -500,6 +567,28 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
var titleFont = Math.Clamp(14 * softScale, 10, 19);
|
||||
var titleMaxWidth = Math.Max(60, innerWidth - avatarSize - (rowPaddingHorizontal * 2d) - 18);
|
||||
|
||||
var estimatedHeaderHeight = Math.Max(
|
||||
Math.Clamp(20 * softScale, 12, 28) + Math.Clamp(4 * softScale, 2, 8),
|
||||
Math.Clamp(34 * softScale, 22, 42));
|
||||
var estimatedRowHeight = avatarSize + (rowPaddingVertical * 2d);
|
||||
var availablePostsHeight = Math.Max(
|
||||
0d,
|
||||
totalHeight -
|
||||
CardBorder.Padding.Top -
|
||||
CardBorder.Padding.Bottom -
|
||||
estimatedHeaderHeight -
|
||||
rowSpacing);
|
||||
var rowFootprint = Math.Max(1d, estimatedRowHeight + rowSpacing);
|
||||
var capacityByHeight = (int)Math.Floor((availablePostsHeight + rowSpacing) / rowFootprint);
|
||||
var resolvedItemCount = Math.Clamp(capacityByHeight, BaseDisplayItemCount, MaxDisplayItemCount);
|
||||
if (scale < 1.08d)
|
||||
{
|
||||
resolvedItemCount = Math.Min(resolvedItemCount, BaseDisplayItemCount);
|
||||
}
|
||||
|
||||
var previousVisibleItemCount = _visibleItemCount;
|
||||
_visibleItemCount = resolvedItemCount;
|
||||
|
||||
foreach (var visual in _itemVisuals)
|
||||
{
|
||||
visual.Host.CornerRadius = new CornerRadius(itemCornerRadius);
|
||||
@@ -516,6 +605,14 @@ public partial class Stcn24ForumWidget : UserControl, IDesktopComponentWidget, I
|
||||
}
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(14 * softScale, 10, 18);
|
||||
|
||||
if (_visibleItemCount != previousVisibleItemCount &&
|
||||
_isAttached &&
|
||||
!_isRefreshing &&
|
||||
_activeItems.Count < _visibleItemCount)
|
||||
{
|
||||
_ = RefreshPostsAsync(forceRefresh: false);
|
||||
}
|
||||
}
|
||||
|
||||
private static string NormalizeCompactText(string? text)
|
||||
|
||||
@@ -754,7 +754,8 @@ public partial class MainWindow
|
||||
return;
|
||||
}
|
||||
|
||||
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyWord)
|
||||
if (placement.ComponentId == BuiltInComponentIds.DesktopDailyWord ||
|
||||
placement.ComponentId == BuiltInComponentIds.DesktopDailyWord2x2)
|
||||
{
|
||||
OpenDailyWordComponentSettings();
|
||||
return;
|
||||
@@ -1131,9 +1132,14 @@ public partial class MainWindow
|
||||
continue;
|
||||
}
|
||||
|
||||
if (TryGetContentHost(host)?.Child is DailyWordWidget widget)
|
||||
var widget = TryGetContentHost(host)?.Child;
|
||||
if (widget is DailyWordWidget dailyWordWidget)
|
||||
{
|
||||
widget.RefreshFromSettings();
|
||||
dailyWordWidget.RefreshFromSettings();
|
||||
}
|
||||
else if (widget is DailyWord2x2Widget dailyWord2x2Widget)
|
||||
{
|
||||
dailyWord2x2Widget.RefreshFromSettings();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
Reference in New Issue
Block a user