新增英语句子组件。优化央广网新闻组件,优化每日单词组件
This commit is contained in:
lincube
2026-03-05 20:17:28 +08:00
parent 3b71486423
commit b8643a2959
14 changed files with 1422 additions and 92 deletions

View File

@@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
@@ -8,6 +9,7 @@ using System.Threading;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Documents;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@@ -43,7 +45,39 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private readonly AppSettingsService _settingsService = new();
private readonly LocalizationService _localizationService = new();
private readonly Bitmap?[] _newsBitmaps = new Bitmap?[2];
private readonly string?[] _newsUrls = new string?[2];
private readonly List<string?> _newsUrls = [];
private readonly List<ExtraNewsRowVisual> _extraNewsRows = [];
private IReadOnlyList<DailyNewsItemSnapshot> _activeNewsItems = [];
private int _renderedNewsCount = 2;
private sealed class ExtraNewsRowVisual
{
public ExtraNewsRowVisual(
Grid rootGrid,
TextBlock titleTextBlock,
Border imageHost,
Image imageControl,
int newsIndex)
{
RootGrid = rootGrid;
TitleTextBlock = titleTextBlock;
ImageHost = imageHost;
ImageControl = imageControl;
NewsIndex = newsIndex;
}
public Grid RootGrid { get; }
public TextBlock TitleTextBlock { get; }
public Border ImageHost { get; }
public Image ImageControl { get; }
public int NewsIndex { get; }
public Bitmap? Bitmap { get; set; }
}
private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
private CancellationTokenSource? _refreshCts;
@@ -60,7 +94,6 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
BrandSecondaryTextBlock.FontFamily = MiSansFontFamily;
RefreshGlyphTextBlock.FontFamily = MiSansFontFamily;
RefreshLabelTextBlock.FontFamily = MiSansFontFamily;
News1PrefixTextBlock.FontFamily = MiSansFontFamily;
News1TitleTextBlock.FontFamily = MiSansFontFamily;
News2TitleTextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily;
@@ -115,12 +148,33 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
_refreshTimer.Stop();
CancelRefreshRequest();
DisposeNewsBitmaps();
ClearExtraNewsRows();
UpdateRefreshButtonState();
}
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
{
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)
@@ -161,6 +215,19 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
e.Handled = true;
}
private void OnExtraNewsItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Control control ||
control.Tag is not int index)
{
return;
}
TryOpenNewsUrl(index);
e.Handled = true;
}
private async Task RefreshNewsAsync(bool forceRefresh)
{
if (!_isAttached || _isRefreshing)
@@ -181,6 +248,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
{
var query = new DailyNewsQuery(
Locale: _languageCode,
ItemCount: ResolveDesiredNewsItemCount(),
ForceRefresh: forceRefresh);
var result = await _recommendationService.GetDailyNewsAsync(query, cts.Token);
if (!_isAttached || cts.IsCancellationRequested)
@@ -222,90 +290,266 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private async Task ApplySnapshotAsync(DailyNewsSnapshot snapshot, CancellationToken cancellationToken)
{
var desiredCount = ResolveDesiredNewsItemCount();
var items = snapshot.Items is null
? []
: snapshot.Items.Take(2).ToArray();
: snapshot.Items.Take(desiredCount).ToArray();
_activeNewsItems = items;
var item1 = items.Length > 0 ? items[0] : null;
var item2 = items.Length > 1 ? items[1] : null;
News1PrefixTextBlock.IsVisible = item1 is not null;
News1TitleTextBlock.Text = NormalizeCompactText(item1?.Title);
UpdateHotHeadlineText(item1?.Title);
News2TitleTextBlock.Text = NormalizeCompactText(item2?.Title);
_newsUrls[0] = NormalizeHttpUrl(item1?.Url);
_newsUrls[1] = NormalizeHttpUrl(item2?.Url);
_newsUrls.Clear();
foreach (var item in items)
{
_newsUrls.Add(NormalizeHttpUrl(item.Url));
}
RenderExtraNewsRows(items.Skip(2).ToArray());
UpdateNewsInteractionState();
StatusTextBlock.IsVisible = false;
UpdateAdaptiveLayout();
var loadTasks = new[]
{
TryDownloadBitmapAsync(item1?.ImageUrl, cancellationToken),
TryDownloadBitmapAsync(item2?.ImageUrl, cancellationToken)
};
var loadTasks = items
.Select(item => TryDownloadBitmapAsync(item.ImageUrl, cancellationToken))
.ToArray();
var bitmaps = await Task.WhenAll(loadTasks);
if (cancellationToken.IsCancellationRequested || !_isAttached)
{
bitmaps[0]?.Dispose();
bitmaps[1]?.Dispose();
foreach (var bitmap in bitmaps)
{
bitmap?.Dispose();
}
return;
}
SetNewsBitmap(0, bitmaps[0]);
SetNewsBitmap(1, bitmaps[1]);
var consumed = new bool[bitmaps.Length];
Bitmap? TakeBitmapAt(int index)
{
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()
{
_newsUrls[0] = null;
_newsUrls[1] = null;
News1PrefixTextBlock.IsVisible = true;
News1TitleTextBlock.Text = L("cnrnews.widget.loading_title", "正在获取新闻热点");
News2TitleTextBlock.Text = L("cnrnews.widget.loading_subtitle", "请稍候");
StatusTextBlock.Text = L("cnrnews.widget.loading", "加载中...");
_activeNewsItems = [];
_newsUrls.Clear();
UpdateHotHeadlineText(L("cnrnews.widget.loading_title", "Loading headlines"));
News2TitleTextBlock.Text = L("cnrnews.widget.loading_subtitle", "Please wait");
StatusTextBlock.Text = L("cnrnews.widget.loading", "Loading...");
StatusTextBlock.IsVisible = true;
SetNewsBitmap(0, null);
SetNewsBitmap(1, null);
RenderExtraNewsRows([]);
UpdateNewsInteractionState();
UpdateAdaptiveLayout();
}
private void ApplyFailedState()
{
_newsUrls[0] = null;
_newsUrls[1] = null;
News1PrefixTextBlock.IsVisible = false;
News1TitleTextBlock.Text = L("cnrnews.widget.fallback_title", "央广网新闻暂不可用");
News2TitleTextBlock.Text = L("cnrnews.widget.fallback_subtitle", "点击右上角稍后重试");
StatusTextBlock.Text = L("cnrnews.widget.fetch_failed", "新闻获取失败");
_activeNewsItems = [];
_newsUrls.Clear();
News1TitleTextBlock.Inlines = null;
News1TitleTextBlock.Text = L("cnrnews.widget.fallback_title", "CNR news is temporarily unavailable");
News2TitleTextBlock.Text = L("cnrnews.widget.fallback_subtitle", "Tap refresh and try again");
StatusTextBlock.Text = L("cnrnews.widget.fetch_failed", "News fetch failed");
StatusTextBlock.IsVisible = true;
SetNewsBitmap(0, null);
SetNewsBitmap(1, null);
RenderExtraNewsRows([]);
UpdateNewsInteractionState();
UpdateAdaptiveLayout();
}
private int ResolveDesiredNewsItemCount()
{
var span = ResolveCurrentCellSpan();
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)
{
var normalizedTitle = NormalizeCompactText(title);
var hotLabel = L("cnrnews.widget.hot_label", "Hot");
if (News1TitleTextBlock.Inlines is null)
{
News1TitleTextBlock.Text = $"{hotLabel} | {normalizedTitle}";
return;
}
News1TitleTextBlock.Inlines.Clear();
News1TitleTextBlock.Inlines.Add(new Run($"{hotLabel} | ")
{
Foreground = new SolidColorBrush(Color.Parse("#D6272E")),
FontWeight = FontWeight.SemiBold
});
News1TitleTextBlock.Inlines.Add(new Run(normalizedTitle)
{
Foreground = new SolidColorBrush(Color.Parse("#202327")),
FontWeight = FontWeight.SemiBold
});
}
private void RenderExtraNewsRows(IReadOnlyList<DailyNewsItemSnapshot> extraItems)
{
ClearExtraNewsRows();
if (extraItems.Count == 0)
{
ExtraNewsItemsPanel.IsVisible = false;
_renderedNewsCount = 2;
return;
}
for (var i = 0; i < extraItems.Count; i++)
{
var item = extraItems[i];
var itemIndex = i + 2;
var rowGrid = new Grid
{
ColumnSpacing = 12,
Tag = itemIndex,
Cursor = new Cursor(StandardCursorType.Hand),
IsHitTestVisible = true
};
rowGrid.ColumnDefinitions.Add(new ColumnDefinition(new GridLength(1, GridUnitType.Star)));
rowGrid.ColumnDefinitions.Add(new ColumnDefinition(GridLength.Auto));
rowGrid.PointerPressed += OnExtraNewsItemPointerPressed;
var textBlock = new TextBlock
{
Text = NormalizeCompactText(item.Title),
Foreground = new SolidColorBrush(Color.Parse("#202327")),
FontFamily = MiSansFontFamily,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top,
IsHitTestVisible = false
};
var imageHost = new Border
{
Width = 160,
Height = 90,
CornerRadius = new CornerRadius(16),
ClipToBounds = true,
Background = new SolidColorBrush(Color.Parse("#E6E6E6")),
IsHitTestVisible = false
};
var image = new Image
{
Stretch = Stretch.UniformToFill,
IsHitTestVisible = false
};
imageHost.Child = image;
Grid.SetColumn(imageHost, 1);
rowGrid.Children.Add(textBlock);
rowGrid.Children.Add(imageHost);
ExtraNewsItemsPanel.Children.Add(rowGrid);
_extraNewsRows.Add(new ExtraNewsRowVisual(rowGrid, textBlock, imageHost, image, itemIndex));
}
ExtraNewsItemsPanel.IsVisible = true;
_renderedNewsCount = 2 + extraItems.Count;
}
private void ClearExtraNewsRows()
{
foreach (var row in _extraNewsRows)
{
row.RootGrid.PointerPressed -= OnExtraNewsItemPointerPressed;
if (ReferenceEquals(row.ImageControl.Source, row.Bitmap))
{
row.ImageControl.Source = null;
}
row.Bitmap?.Dispose();
row.Bitmap = null;
}
_extraNewsRows.Clear();
ExtraNewsItemsPanel.Children.Clear();
}
private void SetExtraNewsBitmap(int rowIndex, Bitmap? bitmap)
{
if (rowIndex < 0 || rowIndex >= _extraNewsRows.Count)
{
bitmap?.Dispose();
return;
}
var row = _extraNewsRows[rowIndex];
if (ReferenceEquals(row.ImageControl.Source, row.Bitmap))
{
row.ImageControl.Source = null;
}
row.Bitmap?.Dispose();
row.Bitmap = bitmap;
row.ImageControl.Source = bitmap;
}
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(34 * scale, 16, 52));
RootBorder.Padding = new Thickness(
Math.Clamp(16 * scale, 8, 28),
Math.Clamp(12 * scale, 6, 20),
Math.Clamp(16 * scale, 8, 28),
Math.Clamp(12 * scale, 6, 20));
RootBorder.Padding = new Thickness(0);
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(24 * scale, 12, 36));
CardBorder.CornerRadius = new CornerRadius(Math.Clamp(34 * scale, 16, 52));
CardBorder.Padding = new Thickness(
Math.Clamp(16 * scale, 8, 24),
Math.Clamp(14 * scale, 7, 22),
Math.Clamp(16 * scale, 8, 24),
Math.Clamp(14 * scale, 7, 22));
var headlineFont = Math.Clamp(28 * scale, 13, 36);
var headlineFont = Math.Clamp(24 * scale, 12, 34);
BrandPrimaryTextBlock.FontSize = headlineFont;
BrandSecondaryTextBlock.FontSize = headlineFont;
@@ -314,10 +558,10 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
RefreshButton.Height = refreshHeight;
RefreshButton.Width = refreshWidth;
RefreshButton.CornerRadius = new CornerRadius(refreshHeight / 2d);
RefreshGlyphTextBlock.FontSize = Math.Clamp(19 * scale, 11, 26);
RefreshLabelTextBlock.FontSize = Math.Clamp(25 * scale, 12, 32);
RefreshGlyphTextBlock.FontSize = Math.Clamp(19 * scale, 11, 24);
RefreshLabelTextBlock.FontSize = Math.Clamp(22 * scale, 11, 29);
var imageWidth = Math.Clamp(totalWidth * 0.23, 68, 170);
var imageWidth = Math.Clamp(totalWidth * 0.20, 60, 170);
var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94);
News1ImageHost.Width = imageWidth;
News1ImageHost.Height = imageHeight;
@@ -332,19 +576,45 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
NewsItem1Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
NewsItem2Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
var availableTextWidth = Math.Max(72, totalWidth - RootBorder.Padding.Left - RootBorder.Padding.Right - imageWidth - columnGap - Math.Clamp(24 * scale, 12, 36));
var availableTextWidth = Math.Max(
84,
totalWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
News1TitleTextBlock.MaxWidth = availableTextWidth;
News2TitleTextBlock.MaxWidth = availableTextWidth;
var newsFont = Math.Clamp(25 * scale, 11, 32);
News1PrefixTextBlock.FontSize = newsFont;
var newsFont = Math.Clamp(21 * scale, 10.5, 28);
News1TitleTextBlock.FontSize = newsFont;
News2TitleTextBlock.FontSize = newsFont;
var mainNewsLineHeight = newsFont * 1.14;
News1TitleTextBlock.LineHeight = mainNewsLineHeight;
News2TitleTextBlock.LineHeight = mainNewsLineHeight;
var mainNewsMinHeight = mainNewsLineHeight * 2;
News1TitleTextBlock.MinHeight = mainNewsMinHeight;
News2TitleTextBlock.MinHeight = mainNewsMinHeight;
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24);
News1TitleTextBlock.MaxLines = 2;
News2TitleTextBlock.MaxLines = 2;
var compactLayout = totalHeight < _currentCellSize * 1.7;
News1TitleTextBlock.MaxLines = compactLayout ? 1 : 2;
News2TitleTextBlock.MaxLines = compactLayout ? 1 : 2;
foreach (var row in _extraNewsRows)
{
row.RootGrid.ColumnSpacing = columnGap;
if (row.RootGrid.ColumnDefinitions.Count > 1)
{
row.RootGrid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
}
row.ImageHost.Width = imageWidth;
row.ImageHost.Height = imageHeight;
row.ImageHost.CornerRadius = new CornerRadius(Math.Clamp(16 * scale, 8, 22));
row.TitleTextBlock.MaxWidth = availableTextWidth;
row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25);
row.TitleTextBlock.LineHeight = row.TitleTextBlock.FontSize * 1.12;
row.TitleTextBlock.MinHeight = row.TitleTextBlock.LineHeight * 2;
row.TitleTextBlock.MaxLines = 2;
}
ExtraNewsItemsPanel.Spacing = Math.Clamp(6 * scale, 3, 10);
}
private void UpdateRefreshButtonState()
@@ -357,13 +627,24 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void UpdateNewsInteractionState()
{
var item1Enabled = !string.IsNullOrWhiteSpace(_newsUrls[0]);
var item2Enabled = !string.IsNullOrWhiteSpace(_newsUrls[1]);
var item1Enabled = _newsUrls.Count > 0 && !string.IsNullOrWhiteSpace(_newsUrls[0]);
var item2Enabled = _newsUrls.Count > 1 && !string.IsNullOrWhiteSpace(_newsUrls[1]);
NewsItem1Grid.IsHitTestVisible = item1Enabled;
NewsItem2Grid.IsHitTestVisible = item2Enabled;
NewsItem1Grid.Opacity = item1Enabled ? 1.0 : 0.72;
NewsItem2Grid.Opacity = item2Enabled ? 1.0 : 0.72;
foreach (var row in _extraNewsRows)
{
var index = row.NewsIndex;
var enabled = index >= 0 && index < _newsUrls.Count && !string.IsNullOrWhiteSpace(_newsUrls[index]);
row.RootGrid.IsHitTestVisible = enabled;
row.RootGrid.Opacity = enabled ? 1.0 : 0.72;
row.RootGrid.Cursor = enabled
? new Cursor(StandardCursorType.Hand)
: new Cursor(StandardCursorType.Arrow);
}
}
private static async Task<Bitmap?> TryDownloadBitmapAsync(string? imageUrl, CancellationToken cancellationToken)
@@ -406,7 +687,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
private void TryOpenNewsUrl(int index)
{
if (index < 0 || index >= _newsUrls.Length)
if (index < 0 || index >= _newsUrls.Count)
{
return;
}
@@ -532,3 +813,4 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
return MultiWhitespaceRegex.Replace(text.Trim(), " ");
}
}