凤凰网新闻组件优化、央广网新闻组件优化。
This commit is contained in:
lincube
2026-03-25 07:44:55 +08:00
parent 26ff11b16b
commit 74703582e7
4 changed files with 296 additions and 344 deletions

View File

@@ -21,7 +21,8 @@
BorderBrush="Transparent" BorderBrush="Transparent"
BorderThickness="0" BorderThickness="0"
Padding="16,14,16,14"> Padding="16,14,16,14">
<Grid RowDefinitions="Auto,Auto,Auto,Auto" <Grid x:Name="ContentGrid"
RowDefinitions="Auto,Auto,Auto,Auto"
RowSpacing="8"> RowSpacing="8">
<Grid Grid.Row="0" <Grid Grid.Row="0"
ColumnDefinitions="*,Auto" ColumnDefinitions="*,Auto"

View File

@@ -625,17 +625,18 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
{ {
var scale = ResolveScale(); var scale = ResolveScale();
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells; var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
var unifiedMainRectangle = ResolveUnifiedMainRectangle(); var unifiedMainRectangle = ResolveUnifiedMainRectangle();
RootBorder.CornerRadius = unifiedMainRectangle; RootBorder.CornerRadius = unifiedMainRectangle;
RootBorder.Padding = new Thickness(0); RootBorder.Padding = new Thickness(0);
var horizontalPadding = Math.Clamp(16 * scale, 8, 24);
var verticalPadding = Math.Clamp(14 * scale, 7, 22);
CardBorder.CornerRadius = unifiedMainRectangle; CardBorder.CornerRadius = unifiedMainRectangle;
CardBorder.Padding = new Thickness( CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
Math.Clamp(16 * scale, 8, 24),
Math.Clamp(14 * scale, 7, 22), var innerWidth = Math.Max(100, totalWidth - horizontalPadding * 2);
Math.Clamp(16 * scale, 8, 24),
Math.Clamp(14 * scale, 7, 22));
var headlineFont = Math.Clamp(24 * scale, 12, 34); var headlineFont = Math.Clamp(24 * scale, 12, 34);
BrandPrimaryTextBlock.FontSize = headlineFont; BrandPrimaryTextBlock.FontSize = headlineFont;
@@ -649,7 +650,7 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
RefreshGlyphIcon.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(innerWidth * 0.22, 60, 170);
var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94); var imageHeight = Math.Clamp(imageWidth * 0.56, 38, 94);
News1ImageHost.Width = imageWidth; News1ImageHost.Width = imageWidth;
News1ImageHost.Height = imageHeight; News1ImageHost.Height = imageHeight;
@@ -657,6 +658,8 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
News2ImageHost.Height = imageHeight; News2ImageHost.Height = imageHeight;
News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); News1ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); News2ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
News1ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
News2ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
var columnGap = Math.Clamp(12 * scale, 6, 18); var columnGap = Math.Clamp(12 * scale, 6, 18);
NewsItem1Grid.ColumnSpacing = columnGap; NewsItem1Grid.ColumnSpacing = columnGap;
@@ -664,25 +667,29 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
NewsItem1Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth); NewsItem1Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
NewsItem2Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth); NewsItem2Grid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
var availableTextWidth = Math.Max( var availableTextWidth = Math.Max(80, innerWidth - imageWidth - columnGap);
84,
totalWidth - imageWidth - columnGap - Math.Clamp(20 * scale, 10, 32));
News1TitleTextBlock.MaxWidth = availableTextWidth; News1TitleTextBlock.MaxWidth = availableTextWidth;
News2TitleTextBlock.MaxWidth = availableTextWidth; News2TitleTextBlock.MaxWidth = availableTextWidth;
var newsFont = Math.Clamp(21 * scale, 10.5, 28); var newsFont = Math.Clamp(21 * scale, 10.5, 28);
News1TitleTextBlock.FontSize = newsFont; News1TitleTextBlock.FontSize = newsFont;
News2TitleTextBlock.FontSize = newsFont; News2TitleTextBlock.FontSize = newsFont;
var mainNewsLineHeight = newsFont * 1.14; var mainNewsLineHeight = newsFont * 1.2;
News1TitleTextBlock.LineHeight = mainNewsLineHeight; News1TitleTextBlock.LineHeight = mainNewsLineHeight;
News2TitleTextBlock.LineHeight = mainNewsLineHeight; News2TitleTextBlock.LineHeight = mainNewsLineHeight;
var mainNewsMinHeight = mainNewsLineHeight * 2; var mainNewsMinHeight = mainNewsLineHeight * 2.2;
News1TitleTextBlock.MinHeight = mainNewsMinHeight; News1TitleTextBlock.MinHeight = mainNewsMinHeight;
News2TitleTextBlock.MinHeight = mainNewsMinHeight; News2TitleTextBlock.MinHeight = mainNewsMinHeight;
StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24); StatusTextBlock.FontSize = Math.Clamp(16 * scale, 9, 24);
News1TitleTextBlock.MaxLines = 2; News1TitleTextBlock.MaxLines = 2;
News2TitleTextBlock.MaxLines = 2; News2TitleTextBlock.MaxLines = 2;
var rowSpacing = Math.Clamp(8 * scale, 4, 14);
if (ContentGrid is Grid contentGrid && contentGrid.RowDefinitions.Count >= 4)
{
contentGrid.RowSpacing = rowSpacing;
}
foreach (var row in _extraNewsRows) foreach (var row in _extraNewsRows)
{ {
row.RootGrid.ColumnSpacing = columnGap; row.RootGrid.ColumnSpacing = columnGap;
@@ -694,11 +701,12 @@ public partial class CnrDailyNewsWidget : UserControl, IDesktopComponentWidget,
row.ImageHost.Width = imageWidth; row.ImageHost.Width = imageWidth;
row.ImageHost.Height = imageHeight; row.ImageHost.Height = imageHeight;
row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22); row.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(16 * scale, 8, 22);
row.ImageHost.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E6E6"));
row.TitleTextBlock.MaxWidth = availableTextWidth; row.TitleTextBlock.MaxWidth = availableTextWidth;
row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25); row.TitleTextBlock.FontSize = Math.Clamp(19 * scale, 10, 25);
row.TitleTextBlock.LineHeight = row.TitleTextBlock.FontSize * 1.12; row.TitleTextBlock.LineHeight = row.TitleTextBlock.FontSize * 1.2;
row.TitleTextBlock.MinHeight = row.TitleTextBlock.LineHeight * 2; row.TitleTextBlock.MinHeight = row.TitleTextBlock.LineHeight * 2.2;
row.TitleTextBlock.MaxLines = 2; row.TitleTextBlock.MaxLines = 2;
} }

View File

@@ -22,19 +22,36 @@
BorderThickness="0" BorderThickness="0"
Padding="14,14,14,14"> Padding="14,14,14,14">
<Grid x:Name="ContentGrid" <Grid x:Name="ContentGrid"
RowDefinitions="Auto,Auto,Auto,Auto,Auto" RowDefinitions="Auto,*">
RowSpacing="8">
<Grid x:Name="HeaderGrid" <Grid x:Name="HeaderGrid"
Grid.Row="0" Grid.Row="0"
ColumnDefinitions="*,Auto" ColumnDefinitions="Auto,*"
ColumnSpacing="10"> ColumnSpacing="10"
<TextBlock x:Name="BrandTextBlock" Margin="0,0,0,8">
Text="凤凰网新闻" <StackPanel Orientation="Horizontal"
Foreground="#E24B2D" Spacing="0"
FontSize="28" VerticalAlignment="Center">
FontWeight="Bold" <TextBlock x:Name="BrandTextBlock"
VerticalAlignment="Center" Text="鳳凰網"
TextTrimming="CharacterEllipsis" /> Foreground="#E24B2D"
FontSize="20"
FontWeight="Bold"
VerticalAlignment="Center" />
<Border x:Name="NewsBadge"
Background="#E24B2D"
CornerRadius="4"
Padding="6,2"
Margin="4,0,0,0"
VerticalAlignment="Center">
<TextBlock x:Name="NewsBadgeText"
Text="新聞"
Foreground="White"
FontSize="20"
FontWeight="Bold"
VerticalAlignment="Center" />
</Border>
</StackPanel>
<Button x:Name="RefreshButton" <Button x:Name="RefreshButton"
Grid.Column="1" Grid.Column="1"
@@ -58,129 +75,18 @@
</Button> </Button>
</Grid> </Grid>
<Border x:Name="NewsItem1Host" <ScrollViewer x:Name="NewsScrollViewer"
Grid.Row="1" Grid.Row="1"
Tag="0" VerticalScrollBarVisibility="Auto">
Background="Transparent" <StackPanel x:Name="NewsStackPanel" Spacing="6">
Padding="0,2" <TextBlock x:Name="LoadingTextBlock"
PointerPressed="OnNewsItemPointerPressed"> Text="正在加载..."
<Grid x:Name="NewsItem1Grid" Foreground="#6A6F77"
ColumnDefinitions="*,Auto" FontSize="14"
ColumnSpacing="10"> HorizontalAlignment="Center"
<TextBlock x:Name="NewsItem1TextBlock" IsVisible="False" />
Text="新闻标题" </StackPanel>
Foreground="#202327" </ScrollViewer>
FontSize="22"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
VerticalAlignment="Top" />
<Border x:Name="NewsItem1ImageHost"
Grid.Column="1"
Width="148"
Height="84"
CornerRadius="12"
ClipToBounds="True"
Background="#E6E8EC">
<Image x:Name="NewsItem1Image"
Stretch="UniformToFill" />
</Border>
</Grid>
</Border>
<Border x:Name="NewsItem2Host"
Grid.Row="2"
Tag="1"
Background="Transparent"
Padding="0,2"
PointerPressed="OnNewsItemPointerPressed">
<Grid x:Name="NewsItem2Grid"
ColumnDefinitions="*,Auto"
ColumnSpacing="10">
<TextBlock x:Name="NewsItem2TextBlock"
Text="新闻标题"
Foreground="#202327"
FontSize="22"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
VerticalAlignment="Top" />
<Border x:Name="NewsItem2ImageHost"
Grid.Column="1"
Width="148"
Height="84"
CornerRadius="12"
ClipToBounds="True"
Background="#E6E8EC">
<Image x:Name="NewsItem2Image"
Stretch="UniformToFill" />
</Border>
</Grid>
</Border>
<Border x:Name="NewsItem3Host"
Grid.Row="3"
Tag="2"
Background="Transparent"
Padding="0,2"
PointerPressed="OnNewsItemPointerPressed">
<Grid x:Name="NewsItem3Grid"
ColumnDefinitions="*,Auto"
ColumnSpacing="10">
<TextBlock x:Name="NewsItem3TextBlock"
Text="新闻标题"
Foreground="#202327"
FontSize="22"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
VerticalAlignment="Top" />
<Border x:Name="NewsItem3ImageHost"
Grid.Column="1"
Width="148"
Height="84"
CornerRadius="12"
ClipToBounds="True"
Background="#E6E8EC">
<Image x:Name="NewsItem3Image"
Stretch="UniformToFill" />
</Border>
</Grid>
</Border>
<Border x:Name="NewsItem4Host"
Grid.Row="4"
Tag="3"
Background="Transparent"
Padding="0,2"
PointerPressed="OnNewsItemPointerPressed">
<Grid x:Name="NewsItem4Grid"
ColumnDefinitions="*,Auto"
ColumnSpacing="10">
<TextBlock x:Name="NewsItem4TextBlock"
Text="新闻标题"
Foreground="#202327"
FontSize="22"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2"
VerticalAlignment="Top" />
<Border x:Name="NewsItem4ImageHost"
Grid.Column="1"
Width="148"
Height="84"
CornerRadius="12"
ClipToBounds="True"
Background="#E6E8EC">
<Image x:Name="NewsItem4Image"
Stretch="UniformToFill" />
</Border>
</Grid>
</Border>
</Grid> </Grid>
</Border> </Border>

View File

@@ -36,7 +36,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
private const double BaseCellSize = 48d; private const double BaseCellSize = 48d;
private const int BaseWidthCells = 4; private const int BaseWidthCells = 4;
private const int BaseHeightCells = 4; private const int BaseHeightCells = 4;
private const int MaxDisplayItemCount = 4; private const int MaxDisplayItemCount = 12;
private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes; private static readonly IReadOnlyList<int> SupportedAutoRefreshIntervalsMinutes = RefreshIntervalCatalog.SupportedIntervalsMinutes;
private readonly DispatcherTimer _refreshTimer = new() private readonly DispatcherTimer _refreshTimer = new()
@@ -47,9 +47,9 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings; private LanMountainDesktop.PluginSdk.ISettingsService _appSettingsService = LanMountainDesktop.Services.Settings.HostSettingsFacadeProvider.GetOrCreate().Settings;
private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate(); private IComponentInstanceSettingsStore _componentSettingsService = HostComponentSettingsStoreProvider.GetOrCreate();
private readonly LocalizationService _localizationService = new(); private readonly LocalizationService _localizationService = new();
private readonly List<DailyNewsItemSnapshot> _activeItems = []; private readonly Dictionary<string, DailyNewsItemSnapshot> _newsByUrl = new(StringComparer.OrdinalIgnoreCase);
private readonly List<NewsItemVisual> _itemVisuals = []; private readonly List<NewsItemControl> _itemControls = [];
private readonly Bitmap?[] _newsBitmaps = new Bitmap?[MaxDisplayItemCount]; private readonly Dictionary<string, Bitmap> _imageCache = new();
private IRecommendationInfoService _recommendationService = DefaultRecommendationService; private IRecommendationInfoService _recommendationService = DefaultRecommendationService;
private CancellationTokenSource? _refreshCts; private CancellationTokenSource? _refreshCts;
@@ -61,28 +61,13 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
private bool _autoRefreshEnabled = true; private bool _autoRefreshEnabled = true;
private bool _isNightVisual = true; private bool _isNightVisual = true;
private sealed record NewsItemVisual(
Border Host,
Grid RowGrid,
TextBlock TitleTextBlock,
Border ImageHost,
Image ImageControl);
public IfengNewsWidget() public IfengNewsWidget()
{ {
InitializeComponent(); InitializeComponent();
BrandTextBlock.FontFamily = MiSansFontFamily; BrandTextBlock.FontFamily = MiSansFontFamily;
NewsItem1TextBlock.FontFamily = MiSansFontFamily;
NewsItem2TextBlock.FontFamily = MiSansFontFamily;
NewsItem3TextBlock.FontFamily = MiSansFontFamily;
NewsItem4TextBlock.FontFamily = MiSansFontFamily;
StatusTextBlock.FontFamily = MiSansFontFamily; StatusTextBlock.FontFamily = MiSansFontFamily;
LoadingTextBlock.FontFamily = MiSansFontFamily;
_itemVisuals.Add(new NewsItemVisual(NewsItem1Host, NewsItem1Grid, NewsItem1TextBlock, NewsItem1ImageHost, NewsItem1Image));
_itemVisuals.Add(new NewsItemVisual(NewsItem2Host, NewsItem2Grid, NewsItem2TextBlock, NewsItem2ImageHost, NewsItem2Image));
_itemVisuals.Add(new NewsItemVisual(NewsItem3Host, NewsItem3Grid, NewsItem3TextBlock, NewsItem3ImageHost, NewsItem3Image));
_itemVisuals.Add(new NewsItemVisual(NewsItem4Host, NewsItem4Grid, NewsItem4TextBlock, NewsItem4ImageHost, NewsItem4Image));
_refreshTimer.Tick += OnRefreshTimerTick; _refreshTimer.Tick += OnRefreshTimerTick;
AttachedToVisualTree += OnAttachedToVisualTree; AttachedToVisualTree += OnAttachedToVisualTree;
@@ -135,7 +120,7 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
_isAttached = false; _isAttached = false;
_refreshTimer.Stop(); _refreshTimer.Stop();
CancelRefreshRequest(); CancelRefreshRequest();
DisposeNewsBitmaps(); DisposeImageCache();
UpdateRefreshButtonState(); UpdateRefreshButtonState();
} }
@@ -191,18 +176,19 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD")); CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#1B2129") : Color.Parse("#FCFCFD"));
RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000")); RootBorder.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#33FFFFFF") : Color.Parse("#00000000"));
BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")); BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#FF6B5A") : Color.Parse("#E24B2D"));
NewsBadge.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#FF6B5A") : Color.Parse("#E24B2D"));
RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5")); RefreshButton.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2D3440") : Color.Parse("#EFF1F5"));
RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671")); RefreshGlyphIcon.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#A8B1C2") : Color.Parse("#5E6671"));
foreach (var visual in _itemVisuals)
{
visual.Host.Background = Brushes.Transparent;
visual.TitleTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
}
StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77")); StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77"));
LoadingTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#8B95A5") : Color.Parse("#6A6F77"));
foreach (var control in _itemControls)
{
control.ApplyNightMode(_isNightVisual);
}
} }
private async void OnRefreshTimerTick(object? sender, EventArgs e) private async void OnRefreshTimerTick(object? sender, EventArgs e)
@@ -217,22 +203,6 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
e.Handled = true; e.Handled = true;
} }
private void OnNewsItemPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed ||
sender is not Border host ||
host.Tag is null ||
!int.TryParse(host.Tag.ToString(), out var index) ||
index < 0 ||
index >= _activeItems.Count)
{
return;
}
TryOpenUrl(_activeItems[index].Url);
e.Handled = true;
}
private async Task RefreshNewsAsync(bool forceRefresh) private async Task RefreshNewsAsync(bool forceRefresh)
{ {
if (!_isAttached || _isRefreshing) if (!_isAttached || _isRefreshing)
@@ -272,7 +242,6 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// Ignore canceled requests.
} }
catch catch
{ {
@@ -296,100 +265,90 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
private async Task ApplySnapshotAsync(DailyNewsSnapshot snapshot, CancellationToken cancellationToken) private async Task ApplySnapshotAsync(DailyNewsSnapshot snapshot, CancellationToken cancellationToken)
{ {
BrandTextBlock.Text = L("ifeng.widget.brand", "凤凰网新闻");
ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新")); ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新"));
_activeItems.Clear(); var newItems = snapshot.Items
foreach (var item in snapshot.Items) .Where(item => !string.IsNullOrWhiteSpace(item.Url) && !_newsByUrl.ContainsKey(item.Url))
.ToList();
if (newItems.Count == 0 && _itemControls.Count == 0)
{ {
if (string.IsNullOrWhiteSpace(item.Title) || string.IsNullOrWhiteSpace(item.Url)) ApplyEmptyState();
{
continue;
}
_activeItems.Add(item);
if (_activeItems.Count >= MaxDisplayItemCount)
{
break;
}
}
var fallbackText = L("ifeng.widget.fallback_item", "暂无新闻");
for (var i = 0; i < _itemVisuals.Count; i++)
{
var visual = _itemVisuals[i];
visual.Host.IsVisible = true;
visual.TitleTextBlock.Text = i < _activeItems.Count
? NormalizeCompactText(_activeItems[i].Title)
: fallbackText;
SetNewsBitmap(i, null);
}
StatusTextBlock.IsVisible = false;
UpdateInteractionState();
UpdateAdaptiveLayout();
var tasks = Enumerable.Range(0, MaxDisplayItemCount)
.Select(index => TryDownloadBitmapAsync(
index < _activeItems.Count ? _activeItems[index].ImageUrl : null,
cancellationToken))
.ToArray();
var bitmaps = await Task.WhenAll(tasks);
if (cancellationToken.IsCancellationRequested || !_isAttached)
{
foreach (var bitmap in bitmaps)
{
bitmap?.Dispose();
}
return; return;
} }
for (var i = 0; i < bitmaps.Length; i++) foreach (var item in newItems)
{ {
SetNewsBitmap(i, bitmaps[i]); _newsByUrl[item.Url] = item;
} }
await Dispatcher.UIThread.InvokeAsync(() =>
{
if (!_isAttached) return;
LoadingTextBlock.IsVisible = false;
StatusTextBlock.IsVisible = false;
foreach (var item in newItems)
{
var control = new NewsItemControl(item, _isNightVisual);
control.Clicked += (s, url) => TryOpenUrl(url);
NewsStackPanel.Children.Insert(NewsStackPanel.Children.Count - 1, control);
_itemControls.Add(control);
}
UpdateAdaptiveLayout();
});
var imageTasks = newItems.Select(async item =>
{
var bitmap = await TryDownloadBitmapAsync(item.ImageUrl, cancellationToken);
if (bitmap != null && !cancellationToken.IsCancellationRequested)
{
await Dispatcher.UIThread.InvokeAsync(() =>
{
if (_imageCache.TryGetValue(item.Url, out var oldBitmap))
{
oldBitmap.Dispose();
}
_imageCache[item.Url] = bitmap;
var control = _itemControls.FirstOrDefault(c => c.NewsUrl == item.Url);
control?.SetImage(bitmap);
});
}
});
await Task.WhenAll(imageTasks);
} }
private void ApplyLoadingState() private void ApplyLoadingState()
{ {
BrandTextBlock.Text = L("ifeng.widget.brand", "凤凰网新闻");
ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新")); ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新"));
_activeItems.Clear(); LoadingTextBlock.Text = L("ifeng.widget.loading", "加载中...");
var loadingText = L("ifeng.widget.loading_item", "加载中..."); LoadingTextBlock.IsVisible = true;
for (var i = 0; i < _itemVisuals.Count; i++) StatusTextBlock.IsVisible = false;
{
var visual = _itemVisuals[i];
visual.Host.IsVisible = true;
visual.TitleTextBlock.Text = loadingText;
SetNewsBitmap(i, null);
}
StatusTextBlock.Text = L("ifeng.widget.loading", "加载中...");
StatusTextBlock.IsVisible = true;
UpdateInteractionState();
UpdateAdaptiveLayout(); UpdateAdaptiveLayout();
} }
private void ApplyFailedState() private void ApplyFailedState()
{ {
BrandTextBlock.Text = L("ifeng.widget.brand", "凤凰网新闻");
ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新")); ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新"));
_activeItems.Clear(); LoadingTextBlock.IsVisible = false;
var fallbackText = L("ifeng.widget.fallback_item", "暂无新闻");
for (var i = 0; i < _itemVisuals.Count; i++)
{
var visual = _itemVisuals[i];
visual.Host.IsVisible = true;
visual.TitleTextBlock.Text = fallbackText;
SetNewsBitmap(i, null);
}
StatusTextBlock.Text = L("ifeng.widget.fetch_failed", "新闻获取失败"); StatusTextBlock.Text = L("ifeng.widget.fetch_failed", "新闻获取失败");
StatusTextBlock.IsVisible = true; StatusTextBlock.IsVisible = true;
UpdateInteractionState(); UpdateAdaptiveLayout();
}
private void ApplyEmptyState()
{
ToolTip.SetTip(RefreshButton, L("ifeng.widget.refresh_tooltip", "刷新"));
LoadingTextBlock.IsVisible = false;
StatusTextBlock.Text = L("ifeng.widget.fallback_item", "暂无新闻");
StatusTextBlock.IsVisible = true;
UpdateAdaptiveLayout(); UpdateAdaptiveLayout();
} }
@@ -408,26 +367,13 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
var verticalPadding = Math.Clamp(14 * softScale, 8, 20); var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding); CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
var rowSpacing = Math.Clamp(8 * softScale, 4, 12); var headerHeight = Math.Clamp(totalHeight * 0.10, 28, 54);
ContentGrid.RowSpacing = rowSpacing; HeaderGrid.Height = headerHeight;
HeaderGrid.ColumnSpacing = Math.Clamp(10 * softScale, 6, 16); HeaderGrid.Margin = new Thickness(0, 0, 0, Math.Clamp(8 * softScale, 4, 12));
var innerWidth = Math.Max(150, totalWidth - horizontalPadding * 2d); var brandFontSize = Math.Clamp(headerHeight * 0.62, 14, 30);
var innerHeight = Math.Max(160, totalHeight - verticalPadding * 2d); BrandTextBlock.FontSize = brandFontSize;
var availableRowsHeight = Math.Max(120, innerHeight - rowSpacing * 4d); NewsBadgeText.FontSize = brandFontSize;
var headerHeight = Math.Clamp(availableRowsHeight * 0.16, 24, 54);
var itemHeight = Math.Max(32, (availableRowsHeight - headerHeight) / 4d);
if (ContentGrid.RowDefinitions.Count >= 5)
{
ContentGrid.RowDefinitions[0].Height = new GridLength(headerHeight);
for (var i = 1; i <= 4; i++)
{
ContentGrid.RowDefinitions[i].Height = new GridLength(itemHeight);
}
}
BrandTextBlock.FontSize = Math.Clamp(headerHeight * 0.62, 14, 30);
var refreshSize = Math.Clamp(headerHeight * 0.84, 22, 44); var refreshSize = Math.Clamp(headerHeight * 0.84, 22, 44);
RefreshButton.Width = refreshSize; RefreshButton.Width = refreshSize;
@@ -435,51 +381,25 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d); RefreshButton.CornerRadius = new CornerRadius(refreshSize / 2d);
RefreshGlyphIcon.FontSize = Math.Clamp(refreshSize * 0.44, 10, 20); RefreshGlyphIcon.FontSize = Math.Clamp(refreshSize * 0.44, 10, 20);
var innerWidth = Math.Max(150, totalWidth - horizontalPadding * 2d);
var imageWidth = Math.Clamp(innerWidth * 0.27, 82, 176); var imageWidth = Math.Clamp(innerWidth * 0.27, 82, 176);
var imageHeight = Math.Clamp(imageWidth * 0.56, 46, 98); var imageHeight = Math.Clamp(imageWidth * 0.56, 46, 98);
var columnGap = Math.Clamp(itemHeight * 0.20, 6, 14);
var rowPadding = Math.Clamp(itemHeight * 0.08, 1, 5);
var textWidth = Math.Max(84, innerWidth - imageWidth - columnGap);
var titleFont = Math.Clamp(itemHeight * 0.32, 12, 24);
foreach (var visual in _itemVisuals) var baseTitleFont = 14;
var areaFactor = (totalWidth * totalHeight) / (BaseWidthCells * BaseCellSize * BaseHeightCells * BaseCellSize);
var adaptiveTitleFont = baseTitleFont * Math.Sqrt(Math.Clamp(areaFactor, 0.6, 2.5));
var titleFont = Math.Clamp(adaptiveTitleFont, 11, 26);
foreach (var control in _itemControls)
{ {
visual.Host.Padding = new Thickness(0, rowPadding, 0, rowPadding); control.UpdateLayout(softScale, innerWidth, imageWidth, imageHeight, titleFont);
visual.RowGrid.ColumnSpacing = columnGap;
if (visual.RowGrid.ColumnDefinitions.Count > 1)
{
visual.RowGrid.ColumnDefinitions[1].Width = new GridLength(imageWidth);
}
visual.ImageHost.Width = imageWidth;
visual.ImageHost.Height = imageHeight;
visual.ImageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16);
visual.TitleTextBlock.MaxWidth = textWidth;
visual.TitleTextBlock.FontSize = titleFont;
visual.TitleTextBlock.LineHeight = titleFont * 1.12;
visual.TitleTextBlock.MinHeight = visual.TitleTextBlock.LineHeight * 2;
visual.TitleTextBlock.MaxLines = 2;
} }
StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 20); StatusTextBlock.FontSize = Math.Clamp(titleFont, 10, 24);
LoadingTextBlock.FontSize = Math.Clamp(titleFont, 10, 24);
ApplyNightModeVisual(); ApplyNightModeVisual();
} }
private void UpdateInteractionState()
{
for (var i = 0; i < _itemVisuals.Count; i++)
{
var visual = _itemVisuals[i];
var enabled = i < _activeItems.Count && !string.IsNullOrWhiteSpace(_activeItems[i].Url);
visual.Host.IsHitTestVisible = enabled;
visual.Host.Opacity = enabled ? 1.0 : 0.68;
visual.Host.Cursor = enabled
? new Cursor(StandardCursorType.Hand)
: new Cursor(StandardCursorType.Arrow);
}
}
private void UpdateRefreshButtonState() private void UpdateRefreshButtonState()
{ {
var enabled = _isAttached && !_isRefreshing; var enabled = _isAttached && !_isRefreshing;
@@ -515,7 +435,6 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
} }
catch catch
{ {
// Keep fallback defaults.
} }
_autoRefreshEnabled = enabled; _autoRefreshEnabled = enabled;
@@ -614,7 +533,6 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
} }
catch catch
{ {
// Ignore malformed URLs or shell launch failures.
} }
} }
@@ -640,32 +558,13 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
return uri.ToString(); return uri.ToString();
} }
private void SetNewsBitmap(int index, Bitmap? bitmap) private void DisposeImageCache()
{ {
if (index < 0 || index >= _newsBitmaps.Length) foreach (var bitmap in _imageCache.Values)
{ {
bitmap?.Dispose(); bitmap.Dispose();
return;
}
var visual = _itemVisuals[index];
var oldBitmap = _newsBitmaps[index];
if (ReferenceEquals(visual.ImageControl.Source, oldBitmap))
{
visual.ImageControl.Source = null;
}
oldBitmap?.Dispose();
_newsBitmaps[index] = bitmap;
visual.ImageControl.Source = bitmap;
}
private void DisposeNewsBitmaps()
{
for (var i = 0; i < _newsBitmaps.Length; i++)
{
SetNewsBitmap(i, null);
} }
_imageCache.Clear();
} }
private double ResolveScale() private double ResolveScale()
@@ -715,4 +614,142 @@ public partial class IfengNewsWidget : UserControl, IDesktopComponentWidget, IRe
cts.Cancel(); cts.Cancel();
cts.Dispose(); cts.Dispose();
} }
private sealed class NewsItemControl : Border
{
private readonly DailyNewsItemSnapshot _item;
private readonly Grid _grid;
private readonly TextBlock _titleTextBlock;
private readonly Border _imageHost;
private readonly Image _imageControl;
private bool _isNightVisual;
private Point _pointerPressedPosition;
private bool _isPointerPressed;
public string NewsUrl => _item.Url;
public NewsItemControl(DailyNewsItemSnapshot item, bool isNightVisual)
{
_item = item;
_isNightVisual = isNightVisual;
Padding = new Thickness(0, 4);
Background = Brushes.Transparent;
Cursor = new Cursor(StandardCursorType.Hand);
PointerPressed += OnPointerPressed;
PointerReleased += OnPointerReleased;
PointerCaptureLost += OnPointerCaptureLost;
_titleTextBlock = new TextBlock
{
Text = NormalizeCompactText(item.Title),
Foreground = new SolidColorBrush(isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327")),
FontFamily = MiSansFontFamily,
FontWeight = FontWeight.SemiBold,
TextWrapping = TextWrapping.Wrap,
TextTrimming = TextTrimming.CharacterEllipsis,
MaxLines = 2,
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Top
};
_imageControl = new Image
{
Stretch = Stretch.UniformToFill
};
_imageHost = new Border
{
Width = 148,
Height = 84,
CornerRadius = new CornerRadius(12),
ClipToBounds = true,
Background = new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E8EC")),
Child = _imageControl
};
_grid = new Grid
{
ColumnDefinitions = ColumnDefinitions.Parse("*,Auto"),
ColumnSpacing = 10
};
Grid.SetColumn(_imageHost, 1);
_grid.Children.Add(_titleTextBlock);
_grid.Children.Add(_imageHost);
Child = _grid;
}
private void OnPointerPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
_isPointerPressed = true;
_pointerPressedPosition = e.GetPosition(this);
e.Handled = true;
}
}
private void OnPointerReleased(object? sender, PointerReleasedEventArgs e)
{
if (!_isPointerPressed)
{
return;
}
_isPointerPressed = false;
var releasePosition = e.GetPosition(this);
var distance = Math.Sqrt(
Math.Pow(releasePosition.X - _pointerPressedPosition.X, 2) +
Math.Pow(releasePosition.Y - _pointerPressedPosition.Y, 2));
if (distance < 5)
{
Clicked?.Invoke(this, _item.Url);
}
e.Handled = true;
}
private void OnPointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
{
_isPointerPressed = false;
}
public void ApplyNightMode(bool isNightVisual)
{
_isNightVisual = isNightVisual;
_titleTextBlock.Foreground = new SolidColorBrush(isNightVisual ? Color.Parse("#E8EAED") : Color.Parse("#202327"));
_imageHost.Background = new SolidColorBrush(isNightVisual ? Color.Parse("#3D4250") : Color.Parse("#E6E8EC"));
}
public void UpdateLayout(double scale, double innerWidth, double imageWidth, double imageHeight, double titleFont)
{
var columnGap = Math.Clamp(imageHeight * 0.20, 6, 14);
_grid.ColumnSpacing = columnGap;
if (_grid.ColumnDefinitions.Count > 1)
{
_grid.ColumnDefinitions[1] = new ColumnDefinition(new GridLength(imageWidth));
}
_imageHost.Width = imageWidth;
_imageHost.Height = imageHeight;
_imageHost.CornerRadius = ComponentChromeCornerRadiusHelper.Scale(imageHeight * 0.15, 8, 16);
var textWidth = Math.Max(84, innerWidth - imageWidth - columnGap);
_titleTextBlock.MaxWidth = textWidth;
_titleTextBlock.FontSize = titleFont;
_titleTextBlock.LineHeight = titleFont * 1.12;
_titleTextBlock.MinHeight = _titleTextBlock.LineHeight * 2;
}
public void SetImage(Bitmap bitmap)
{
_imageControl.Source = bitmap;
}
public event EventHandler<string>? Clicked;
}
} }