mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-06-22 17:24:27 +08:00
0.7.7
橘鸦新闻
This commit is contained in:
1
LanMountainDesktop/Assets/bilibili.svg
Normal file
1
LanMountainDesktop/Assets/bilibili.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>Bilibili</title><path d="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
BIN
LanMountainDesktop/Assets/juya_avatar.jpg
Normal file
BIN
LanMountainDesktop/Assets/juya_avatar.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 64 KiB |
1
LanMountainDesktop/Assets/wechat.svg
Normal file
1
LanMountainDesktop/Assets/wechat.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg role="img" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><title>WeChat</title><path d="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -33,6 +33,7 @@ public static class BuiltInComponentIds
|
||||
public const string DesktopDailyWord2x2 = "DesktopDailyWord2x2";
|
||||
public const string DesktopCnrDailyNews = "DesktopCnrDailyNews";
|
||||
public const string DesktopIfengNews = "DesktopIfengNews";
|
||||
public const string DesktopJuyaNews = "DesktopJuyaNews";
|
||||
public const string DesktopBilibiliHotSearch = "DesktopBilibiliHotSearch";
|
||||
public const string DesktopBaiduHotSearch = "DesktopBaiduHotSearch";
|
||||
public const string DesktopStcn24Forum = "DesktopStcn24Forum";
|
||||
|
||||
@@ -261,6 +261,16 @@ public sealed class ComponentRegistry
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopJuyaNews,
|
||||
"橘鸦早报",
|
||||
"News",
|
||||
"Info",
|
||||
MinWidthCells: 4,
|
||||
MinHeightCells: 4,
|
||||
AllowStatusBarPlacement: false,
|
||||
AllowDesktopPlacement: true,
|
||||
ResizeMode: DesktopComponentResizeMode.Free),
|
||||
new DesktopComponentDefinition(
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
"Bilibili Hot Search",
|
||||
|
||||
105
LanMountainDesktop/Views/Components/DailyNewsView.axaml
Normal file
105
LanMountainDesktop/Views/Components/DailyNewsView.axaml
Normal file
@@ -0,0 +1,105 @@
|
||||
<UserControl xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
mc:Ignorable="d"
|
||||
x:Class="LanMountainDesktop.Views.Components.DailyNewsView">
|
||||
|
||||
<UserControl.Styles>
|
||||
<Style Selector="Button.link-button">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Padding" Value="4"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
|
||||
<StackPanel x:Name="RootStackPanel" Spacing="16">
|
||||
<Border x:Name="CoverImageBorder"
|
||||
CornerRadius="12"
|
||||
ClipToBounds="True"
|
||||
Background="#f8f5ec"
|
||||
PointerPressed="OnCoverImagePointerPressed"
|
||||
Cursor="Hand">
|
||||
<Image x:Name="CoverImage"
|
||||
Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="DateTextBlock"
|
||||
Grid.Column="0"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
Foreground="#bb5649"
|
||||
VerticalAlignment="Center"/>
|
||||
|
||||
<StackPanel Grid.Column="1"
|
||||
Orientation="Horizontal"
|
||||
Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<Button x:Name="BilibiliButton"
|
||||
Classes="link-button"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
CornerRadius="16"
|
||||
Background="#FB7299"
|
||||
Cursor="Hand"
|
||||
Click="OnBilibiliButtonClick"
|
||||
ToolTip.Tip="观看视频版">
|
||||
<Path Stretch="Uniform"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Fill="White"
|
||||
Data="M17.813 4.653h.854c1.51.054 2.769.578 3.773 1.574 1.004.995 1.524 2.249 1.56 3.76v7.36c-.036 1.51-.556 2.769-1.56 3.773s-2.262 1.524-3.773 1.56H5.333c-1.51-.036-2.769-.556-3.773-1.56S.036 18.858 0 17.347v-7.36c.036-1.511.556-2.765 1.56-3.76 1.004-.996 2.262-1.52 3.773-1.574h.774l-1.174-1.12a1.234 1.234 0 0 1-.373-.906c0-.356.124-.658.373-.907l.027-.027c.267-.249.573-.373.92-.373.347 0 .653.124.92.373L9.653 4.44c.071.071.134.142.187.213h4.267a.836.836 0 0 1 .16-.213l2.853-2.747c.267-.249.573-.373.92-.373.347 0 .662.151.929.4.267.249.391.551.391.907 0 .355-.124.657-.373.906zM5.333 7.24c-.746.018-1.373.276-1.88.773-.506.498-.769 1.13-.786 1.894v7.52c.017.764.28 1.395.786 1.893.507.498 1.134.756 1.88.773h13.334c.746-.017 1.373-.275 1.88-.773.506-.498.769-1.129.786-1.893v-7.52c-.017-.765-.28-1.396-.786-1.894-.507-.497-1.134-.755-1.88-.773zM8 11.107c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c0-.373.129-.689.386-.947.258-.257.574-.386.947-.386zm8 0c.373 0 .684.124.933.373.25.249.383.569.4.96v1.173c-.017.391-.15.711-.4.96-.249.25-.56.374-.933.374s-.684-.125-.933-.374c-.25-.249-.383-.569-.4-.96V12.44c.017-.391.15-.711.4-.96.249-.249.56-.373.933-.373Z"/>
|
||||
</Button>
|
||||
|
||||
<Button x:Name="WechatButton"
|
||||
Classes="link-button"
|
||||
Width="32"
|
||||
Height="32"
|
||||
Padding="0"
|
||||
CornerRadius="16"
|
||||
Background="#07C160"
|
||||
Cursor="Hand"
|
||||
Click="OnWechatButtonClick"
|
||||
ToolTip.Tip="阅读原文">
|
||||
<Path Stretch="Uniform"
|
||||
Width="18"
|
||||
Height="18"
|
||||
Fill="White"
|
||||
Data="M8.691 2.188C3.891 2.188 0 5.476 0 9.53c0 2.212 1.17 4.203 3.002 5.55a.59.59 0 0 1 .213.665l-.39 1.48c-.019.07-.048.141-.048.213 0 .163.13.295.29.295a.326.326 0 0 0 .167-.054l1.903-1.114a.864.864 0 0 1 .717-.098 10.16 10.16 0 0 0 2.837.403c.276 0 .543-.027.811-.05-.857-2.578.157-4.972 1.932-6.446 1.703-1.415 3.882-1.98 5.853-1.838-.576-3.583-4.196-6.348-8.596-6.348zM5.785 5.991c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178A1.17 1.17 0 0 1 4.623 7.17c0-.651.52-1.18 1.162-1.18zm5.813 0c.642 0 1.162.529 1.162 1.18a1.17 1.17 0 0 1-1.162 1.178 1.17 1.17 0 0 1-1.162-1.178c0-.651.52-1.18 1.162-1.18zm5.34 2.867c-1.797-.052-3.746.512-5.28 1.786-1.72 1.428-2.687 3.72-1.78 6.22.942 2.453 3.666 4.229 6.884 4.229.826 0 1.622-.12 2.361-.336a.722.722 0 0 1 .598.082l1.584.926a.272.272 0 0 0 .14.047c.134 0 .24-.111.24-.247 0-.06-.023-.12-.038-.177l-.327-1.233a.582.582 0 0 1-.023-.156.49.49 0 0 1 .201-.398C23.024 18.48 24 16.82 24 14.98c0-3.21-2.931-5.837-6.656-6.088V8.89c-.135-.01-.27-.027-.407-.03zm-2.53 3.274c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.97-.982zm4.844 0c.535 0 .969.44.969.982a.976.976 0 0 1-.969.983.976.976 0 0 1-.969-.983c0-.542.434-.982.969-.982z"/>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Border x:Name="OverviewBorder"
|
||||
Background="#f8f5ec"
|
||||
CornerRadius="8"
|
||||
Padding="12"
|
||||
Margin="0,0,0,8">
|
||||
<StackPanel x:Name="OverviewStackPanel" Spacing="12"/>
|
||||
</Border>
|
||||
|
||||
<Button x:Name="ShowMoreButton"
|
||||
Content="展开更多新闻 ▼"
|
||||
FontSize="14"
|
||||
Padding="16,8"
|
||||
CornerRadius="8"
|
||||
Background="Transparent"
|
||||
BorderBrush="#bb5649"
|
||||
BorderThickness="1"
|
||||
Foreground="#bb5649"
|
||||
Cursor="Hand"
|
||||
Click="OnShowMoreButtonClick"/>
|
||||
|
||||
<StackPanel x:Name="DetailedNewsStackPanel"
|
||||
Spacing="16"
|
||||
IsVisible="False"/>
|
||||
|
||||
<Border x:Name="DateSeparatorBorder"
|
||||
Height="1"
|
||||
Background="#e6e6e6"
|
||||
Margin="0,8,0,0"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
526
LanMountainDesktop/Views/Components/DailyNewsView.axaml.cs
Normal file
526
LanMountainDesktop/Views/Components/DailyNewsView.axaml.cs
Normal file
@@ -0,0 +1,526 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class DailyNewsView : UserControl
|
||||
{
|
||||
private static readonly HttpClient HttpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(10)
|
||||
};
|
||||
|
||||
private readonly JuyaDailyNews _news;
|
||||
private Bitmap? _coverBitmap;
|
||||
private bool _isNightMode;
|
||||
private bool _isExpanded;
|
||||
|
||||
public event EventHandler? CoverImageClicked;
|
||||
public event EventHandler<string>? NewsItemClicked;
|
||||
|
||||
public DailyNewsView(JuyaDailyNews news, bool isNightMode)
|
||||
{
|
||||
InitializeComponent();
|
||||
_news = news;
|
||||
_isNightMode = isNightMode;
|
||||
|
||||
var dateStr = news.Date.ToString("yyyy年M月d日");
|
||||
var dayOfWeek = news.Date.ToString("dddd");
|
||||
DateTextBlock.Text = $"{dateStr} {dayOfWeek}";
|
||||
|
||||
_ = LoadCoverImageAsync(news.CoverImageUrl);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(news.BilibiliUrl))
|
||||
{
|
||||
BilibiliButton.IsVisible = false;
|
||||
}
|
||||
|
||||
if (string.IsNullOrWhiteSpace(news.IssueUrl))
|
||||
{
|
||||
WechatButton.IsVisible = false;
|
||||
}
|
||||
|
||||
if (news.OverviewCategories.Any())
|
||||
{
|
||||
foreach (var category in news.OverviewCategories)
|
||||
{
|
||||
var categoryPanel = new StackPanel { Spacing = 6 };
|
||||
|
||||
var categoryHeader = new TextBlock
|
||||
{
|
||||
Text = $"{category.Icon} {category.Name}",
|
||||
FontSize = 15,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = new SolidColorBrush(isNightMode ? Color.Parse("#d4736a") : Color.Parse("#bb5649"))
|
||||
};
|
||||
categoryPanel.Children.Add(categoryHeader);
|
||||
|
||||
foreach (var item in category.Items)
|
||||
{
|
||||
var itemPanel = new StackPanel
|
||||
{
|
||||
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||
Spacing = 4
|
||||
};
|
||||
|
||||
var bulletText = new TextBlock
|
||||
{
|
||||
Text = "•",
|
||||
FontSize = 13,
|
||||
Foreground = new SolidColorBrush(isNightMode ? Color.Parse("#9a9590") : Color.Parse("#757575"))
|
||||
};
|
||||
itemPanel.Children.Add(bulletText);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(item.Url))
|
||||
{
|
||||
var linkButton = new HyperlinkButton
|
||||
{
|
||||
Content = item.Title,
|
||||
NavigateUri = new Uri(item.Url),
|
||||
FontSize = 13,
|
||||
Foreground = new SolidColorBrush(isNightMode ? Color.Parse("#9a9590") : Color.Parse("#757575")),
|
||||
Padding = new Thickness(0)
|
||||
};
|
||||
itemPanel.Children.Add(linkButton);
|
||||
}
|
||||
else
|
||||
{
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = item.Title,
|
||||
FontSize = 13,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(isNightMode ? Color.Parse("#9a9590") : Color.Parse("#757575"))
|
||||
};
|
||||
itemPanel.Children.Add(titleText);
|
||||
}
|
||||
|
||||
if (item.Number.HasValue)
|
||||
{
|
||||
var numberText = new TextBlock
|
||||
{
|
||||
Text = $"#{item.Number}",
|
||||
FontSize = 11,
|
||||
Foreground = new SolidColorBrush(isNightMode ? Color.Parse("#d4736a") : Color.Parse("#bb5649")),
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
itemPanel.Children.Add(numberText);
|
||||
}
|
||||
|
||||
categoryPanel.Children.Add(itemPanel);
|
||||
}
|
||||
|
||||
OverviewStackPanel.Children.Add(categoryPanel);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
OverviewBorder.IsVisible = false;
|
||||
}
|
||||
|
||||
if (!news.DetailedNews.Any())
|
||||
{
|
||||
ShowMoreButton.IsVisible = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var detailedItem in news.DetailedNews)
|
||||
{
|
||||
var newsPanel = CreateDetailedNewsPanel(detailedItem, isNightMode);
|
||||
DetailedNewsStackPanel.Children.Add(newsPanel);
|
||||
}
|
||||
}
|
||||
|
||||
ApplyNightMode(isNightMode);
|
||||
}
|
||||
|
||||
private Border CreateDetailedNewsPanel(JuyaDetailedNewsItem detailedItem, bool isNightMode)
|
||||
{
|
||||
var primaryColor = isNightMode ? "#d4736a" : "#bb5649";
|
||||
var textColor = isNightMode ? "#e8e4e0" : "#34495e";
|
||||
var secondaryTextColor = isNightMode ? "#9a9590" : "#757575";
|
||||
|
||||
var mainBorder = new Border
|
||||
{
|
||||
Background = Brushes.Transparent,
|
||||
BorderBrush = new SolidColorBrush(Color.Parse("#e6e6e6")),
|
||||
BorderThickness = new Thickness(0, 0, 0, 1),
|
||||
Padding = new Thickness(0, 0, 0, 16)
|
||||
};
|
||||
|
||||
var mainStack = new StackPanel { Spacing = 12 };
|
||||
mainBorder.Child = mainStack;
|
||||
|
||||
var headerPanel = new StackPanel
|
||||
{
|
||||
Orientation = Avalonia.Layout.Orientation.Horizontal,
|
||||
Spacing = 8
|
||||
};
|
||||
|
||||
if (detailedItem.Number > 0)
|
||||
{
|
||||
var numberBadge = new Border
|
||||
{
|
||||
Background = new SolidColorBrush(Color.Parse(primaryColor)),
|
||||
CornerRadius = new CornerRadius(4),
|
||||
Padding = new Thickness(6, 2),
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
var numberText = new TextBlock
|
||||
{
|
||||
Text = $"#{detailedItem.Number}",
|
||||
FontSize = 12,
|
||||
FontWeight = FontWeight.Bold,
|
||||
Foreground = Brushes.White
|
||||
};
|
||||
numberBadge.Child = numberText;
|
||||
headerPanel.Children.Add(numberBadge);
|
||||
}
|
||||
|
||||
var titleText = new TextBlock
|
||||
{
|
||||
Text = detailedItem.Title,
|
||||
FontSize = 16,
|
||||
FontWeight = FontWeight.SemiBold,
|
||||
Foreground = new SolidColorBrush(Color.Parse(textColor)),
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
VerticalAlignment = Avalonia.Layout.VerticalAlignment.Center
|
||||
};
|
||||
headerPanel.Children.Add(titleText);
|
||||
mainStack.Children.Add(headerPanel);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(detailedItem.BodyText))
|
||||
{
|
||||
var bodyText = new TextBlock
|
||||
{
|
||||
Text = detailedItem.BodyText,
|
||||
FontSize = 14,
|
||||
LineHeight = 22,
|
||||
TextWrapping = TextWrapping.Wrap,
|
||||
Foreground = new SolidColorBrush(Color.Parse(textColor))
|
||||
};
|
||||
mainStack.Children.Add(bodyText);
|
||||
}
|
||||
|
||||
if (detailedItem.RelatedLinks.Any())
|
||||
{
|
||||
var linksPanel = new StackPanel { Spacing = 4 };
|
||||
|
||||
var linksHeader = new TextBlock
|
||||
{
|
||||
Text = "相关链接:",
|
||||
FontSize = 12,
|
||||
Foreground = new SolidColorBrush(Color.Parse(secondaryTextColor))
|
||||
};
|
||||
linksPanel.Children.Add(linksHeader);
|
||||
|
||||
foreach (var link in detailedItem.RelatedLinks.Take(3))
|
||||
{
|
||||
var linkButton = new HyperlinkButton
|
||||
{
|
||||
Content = link.Length > 50 ? link.Substring(0, 50) + "..." : link,
|
||||
NavigateUri = new Uri(link),
|
||||
FontSize = 12,
|
||||
Foreground = new SolidColorBrush(Color.Parse(primaryColor))
|
||||
};
|
||||
linksPanel.Children.Add(linkButton);
|
||||
}
|
||||
|
||||
mainStack.Children.Add(linksPanel);
|
||||
}
|
||||
|
||||
return mainBorder;
|
||||
}
|
||||
|
||||
private void OnShowMoreButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
_isExpanded = !_isExpanded;
|
||||
DetailedNewsStackPanel.IsVisible = _isExpanded;
|
||||
ShowMoreButton.Content = _isExpanded ? "收起新闻 ▲" : "展开更多新闻 ▼";
|
||||
}
|
||||
|
||||
private void OnBilibiliButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_news.BilibiliUrl))
|
||||
{
|
||||
TryOpenUrl(_news.BilibiliUrl);
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnWechatButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_news.IssueUrl))
|
||||
{
|
||||
TryOpenUrl(_news.IssueUrl);
|
||||
}
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private static void TryOpenUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadCoverImageAsync(string? imageUrl)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(imageUrl))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var response = await HttpClient.GetAsync(imageUrl);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
await using var stream = await response.Content.ReadAsStreamAsync();
|
||||
var bitmap = new Bitmap(stream);
|
||||
_coverBitmap = bitmap;
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
CoverImage.Source = bitmap;
|
||||
});
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private void OnCoverImagePointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
CoverImageClicked?.Invoke(this, EventArgs.Empty);
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void ApplyNightMode(bool isNightMode)
|
||||
{
|
||||
_isNightMode = isNightMode;
|
||||
var primaryColor = isNightMode ? "#d4736a" : "#bb5649";
|
||||
var textColor = isNightMode ? "#e8e4e0" : "#34495e";
|
||||
var secondaryTextColor = isNightMode ? "#9a9590" : "#757575";
|
||||
var separatorColor = isNightMode ? "#3d3a3a" : "#e6e6e6";
|
||||
var coverBgColor = isNightMode ? "#3d3a3a" : "#f8f5ec";
|
||||
var overviewBgColor = isNightMode ? "#3d3a3a" : "#f8f5ec";
|
||||
|
||||
DateTextBlock.Foreground = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
DateSeparatorBorder.Background = new SolidColorBrush(Color.Parse(separatorColor));
|
||||
CoverImageBorder.Background = new SolidColorBrush(Color.Parse(coverBgColor));
|
||||
OverviewBorder.Background = new SolidColorBrush(Color.Parse(overviewBgColor));
|
||||
|
||||
ShowMoreButton.BorderBrush = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
ShowMoreButton.Foreground = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
|
||||
foreach (var child in OverviewStackPanel.Children)
|
||||
{
|
||||
if (child is StackPanel categoryPanel && categoryPanel.Children.Count > 0)
|
||||
{
|
||||
if (categoryPanel.Children[0] is TextBlock categoryHeader)
|
||||
{
|
||||
categoryHeader.Foreground = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
}
|
||||
|
||||
for (int i = 1; i < categoryPanel.Children.Count; i++)
|
||||
{
|
||||
if (categoryPanel.Children[i] is StackPanel itemPanel)
|
||||
{
|
||||
foreach (var itemChild in itemPanel.Children)
|
||||
{
|
||||
if (itemChild is TextBlock textBlock)
|
||||
{
|
||||
if (textBlock.Text.StartsWith("#"))
|
||||
{
|
||||
textBlock.Foreground = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
}
|
||||
else
|
||||
{
|
||||
textBlock.Foreground = new SolidColorBrush(Color.Parse(secondaryTextColor));
|
||||
}
|
||||
}
|
||||
else if (itemChild is HyperlinkButton linkBtn)
|
||||
{
|
||||
linkBtn.Foreground = new SolidColorBrush(Color.Parse(secondaryTextColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in DetailedNewsStackPanel.Children)
|
||||
{
|
||||
if (child is Border mainBorder && mainBorder.Child is StackPanel mainStack)
|
||||
{
|
||||
mainBorder.BorderBrush = new SolidColorBrush(Color.Parse(separatorColor));
|
||||
|
||||
foreach (var stackChild in mainStack.Children)
|
||||
{
|
||||
if (stackChild is StackPanel headerPanel)
|
||||
{
|
||||
foreach (var headerChild in headerPanel.Children)
|
||||
{
|
||||
if (headerChild is Border numberBadge && numberBadge.Child is TextBlock numberText)
|
||||
{
|
||||
numberBadge.Background = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
}
|
||||
else if (headerChild is TextBlock titleText)
|
||||
{
|
||||
titleText.Foreground = new SolidColorBrush(Color.Parse(textColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stackChild is TextBlock bodyText)
|
||||
{
|
||||
bodyText.Foreground = new SolidColorBrush(Color.Parse(textColor));
|
||||
}
|
||||
else if (stackChild is StackPanel linksPanel)
|
||||
{
|
||||
foreach (var linkChild in linksPanel.Children)
|
||||
{
|
||||
if (linkChild is TextBlock linksHeader)
|
||||
{
|
||||
linksHeader.Foreground = new SolidColorBrush(Color.Parse(secondaryTextColor));
|
||||
}
|
||||
else if (linkChild is HyperlinkButton linkButton)
|
||||
{
|
||||
linkButton.Foreground = new SolidColorBrush(Color.Parse(primaryColor));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateLayout(double scale, double availableWidth)
|
||||
{
|
||||
var coverHeight = availableWidth * 9 / 16;
|
||||
CoverImageBorder.Width = availableWidth;
|
||||
CoverImageBorder.Height = coverHeight;
|
||||
|
||||
DateTextBlock.FontSize = Math.Clamp(20 * scale, 16, 26);
|
||||
|
||||
ShowMoreButton.FontSize = Math.Clamp(14 * scale, 12, 16);
|
||||
|
||||
var buttonSize = Math.Clamp(32 * scale, 24, 40);
|
||||
BilibiliButton.Width = buttonSize;
|
||||
BilibiliButton.Height = buttonSize;
|
||||
BilibiliButton.CornerRadius = new CornerRadius(buttonSize / 2);
|
||||
|
||||
WechatButton.Width = buttonSize;
|
||||
WechatButton.Height = buttonSize;
|
||||
WechatButton.CornerRadius = new CornerRadius(buttonSize / 2);
|
||||
|
||||
foreach (var child in OverviewStackPanel.Children)
|
||||
{
|
||||
if (child is StackPanel categoryPanel && categoryPanel.Children.Count > 0)
|
||||
{
|
||||
if (categoryPanel.Children[0] is TextBlock categoryHeader)
|
||||
{
|
||||
categoryHeader.FontSize = Math.Clamp(15 * scale, 13, 18);
|
||||
}
|
||||
|
||||
for (int i = 1; i < categoryPanel.Children.Count; i++)
|
||||
{
|
||||
if (categoryPanel.Children[i] is StackPanel itemPanel)
|
||||
{
|
||||
foreach (var itemChild in itemPanel.Children)
|
||||
{
|
||||
if (itemChild is TextBlock textBlock)
|
||||
{
|
||||
textBlock.FontSize = Math.Clamp(13 * scale, 11, 15);
|
||||
}
|
||||
else if (itemChild is HyperlinkButton linkBtn)
|
||||
{
|
||||
linkBtn.FontSize = Math.Clamp(13 * scale, 11, 15);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var child in DetailedNewsStackPanel.Children)
|
||||
{
|
||||
if (child is Border mainBorder && mainBorder.Child is StackPanel mainStack)
|
||||
{
|
||||
foreach (var stackChild in mainStack.Children)
|
||||
{
|
||||
if (stackChild is StackPanel headerPanel)
|
||||
{
|
||||
foreach (var headerChild in headerPanel.Children)
|
||||
{
|
||||
if (headerChild is Border numberBadge && numberBadge.Child is TextBlock numberText)
|
||||
{
|
||||
numberText.FontSize = Math.Clamp(12 * scale, 10, 14);
|
||||
}
|
||||
else if (headerChild is TextBlock titleText)
|
||||
{
|
||||
titleText.FontSize = Math.Clamp(16 * scale, 14, 20);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (stackChild is TextBlock bodyText)
|
||||
{
|
||||
bodyText.FontSize = Math.Clamp(14 * scale, 12, 16);
|
||||
bodyText.LineHeight = 22 * scale;
|
||||
}
|
||||
else if (stackChild is StackPanel linksPanel)
|
||||
{
|
||||
foreach (var linkChild in linksPanel.Children)
|
||||
{
|
||||
if (linkChild is TextBlock linksHeader)
|
||||
{
|
||||
linksHeader.FontSize = Math.Clamp(12 * scale, 10, 14);
|
||||
}
|
||||
else if (linkChild is HyperlinkButton linkButton)
|
||||
{
|
||||
linkButton.FontSize = Math.Clamp(12 * scale, 10, 14);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
base.OnDetachedFromVisualTree(e);
|
||||
_coverBitmap?.Dispose();
|
||||
_coverBitmap = null;
|
||||
}
|
||||
}
|
||||
@@ -428,6 +428,10 @@ public sealed class DesktopComponentRuntimeRegistry
|
||||
BuiltInComponentIds.DesktopIfengNews,
|
||||
"component.ifeng_news",
|
||||
() => new IfengNewsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopJuyaNews,
|
||||
"component.juya_news",
|
||||
() => new JuyaNewsWidget()),
|
||||
new DesktopComponentRuntimeRegistration(
|
||||
BuiltInComponentIds.DesktopBilibiliHotSearch,
|
||||
"component.bilibili_hot_search",
|
||||
|
||||
104
LanMountainDesktop/Views/Components/JuyaNewsWidget.axaml
Normal file
104
LanMountainDesktop/Views/Components/JuyaNewsWidget.axaml
Normal file
@@ -0,0 +1,104 @@
|
||||
<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="640"
|
||||
d:DesignHeight="640"
|
||||
x:Class="LanMountainDesktop.Views.Components.JuyaNewsWidget">
|
||||
|
||||
<Border x:Name="RootBorder"
|
||||
CornerRadius="24"
|
||||
Background="Transparent"
|
||||
ClipToBounds="True"
|
||||
BorderThickness="0"
|
||||
Padding="0">
|
||||
<Grid>
|
||||
<Border x:Name="CardBorder"
|
||||
Background="#fefefe"
|
||||
CornerRadius="24"
|
||||
BorderBrush="Transparent"
|
||||
BorderThickness="0"
|
||||
Padding="16,14,16,14">
|
||||
<Grid x:Name="ContentGrid"
|
||||
RowDefinitions="Auto,*">
|
||||
|
||||
<!-- Header -->
|
||||
<Grid x:Name="HeaderGrid"
|
||||
Grid.Row="0"
|
||||
ColumnDefinitions="*,Auto"
|
||||
ColumnSpacing="10"
|
||||
Margin="0,0,0,12">
|
||||
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" Spacing="10">
|
||||
<Border x:Name="AvatarBorder"
|
||||
Width="36"
|
||||
Height="36"
|
||||
CornerRadius="18"
|
||||
ClipToBounds="True"
|
||||
Background="#f8f5ec">
|
||||
<Image x:Name="AvatarImage"
|
||||
Source="avares://LanMountainDesktop/Assets/juya_avatar.jpg"
|
||||
Stretch="UniformToFill"/>
|
||||
</Border>
|
||||
<TextBlock x:Name="BrandTextBlock"
|
||||
Text="橘鸦Juya"
|
||||
Foreground="#bb5649"
|
||||
FontSize="20"
|
||||
FontWeight="Bold"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
|
||||
<Button x:Name="RefreshButton"
|
||||
Grid.Column="1"
|
||||
Padding="8,4"
|
||||
CornerRadius="8"
|
||||
Background="Transparent"
|
||||
BorderBrush="#bb5649"
|
||||
BorderThickness="1"
|
||||
Foreground="#bb5649"
|
||||
Focusable="False"
|
||||
ToolTip.Tip="刷新新闻"
|
||||
Click="OnRefreshButtonClick">
|
||||
<StackPanel Orientation="Horizontal" Spacing="4">
|
||||
<fi:SymbolIcon Symbol="ArrowSync"
|
||||
IconVariant="Regular"
|
||||
FontSize="14"
|
||||
Foreground="#bb5649" />
|
||||
<TextBlock Text="刷新"
|
||||
FontSize="13"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
</Grid>
|
||||
|
||||
<!-- 滚动内容区 -->
|
||||
<ScrollViewer x:Name="ContentScrollViewer"
|
||||
Grid.Row="1"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
ScrollChanged="OnScrollChanged">
|
||||
<StackPanel x:Name="NewsStackPanel" Spacing="16">
|
||||
|
||||
<!-- 加载提示 -->
|
||||
<TextBlock x:Name="LoadingTextBlock"
|
||||
Text="正在加载..."
|
||||
Foreground="#757575"
|
||||
FontSize="14"
|
||||
HorizontalAlignment="Center"
|
||||
IsVisible="False" />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock x:Name="StatusTextBlock"
|
||||
IsVisible="False"
|
||||
Text="Loading"
|
||||
Foreground="#757575"
|
||||
FontSize="16"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</Border>
|
||||
</UserControl>
|
||||
756
LanMountainDesktop/Views/Components/JuyaNewsWidget.axaml.cs
Normal file
756
LanMountainDesktop/Views/Components/JuyaNewsWidget.axaml.cs
Normal file
@@ -0,0 +1,756 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Xml.Linq;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Styling;
|
||||
using Avalonia.Threading;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Services;
|
||||
|
||||
namespace LanMountainDesktop.Views.Components;
|
||||
|
||||
public partial class JuyaNewsWidget : UserControl, IDesktopComponentWidget
|
||||
{
|
||||
private static readonly FontFamily MiSansFontFamily = new("MiSans VF, avares://LanMountainDesktop/Assets/Fonts#MiSans");
|
||||
private static readonly HttpClient HttpClient = new()
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(15)
|
||||
};
|
||||
|
||||
private const string RssUrl = "https://imjuya.github.io/juya-ai-daily/rss.xml";
|
||||
private const double BaseCellSize = 48d;
|
||||
private const int BaseWidthCells = 4;
|
||||
private const int BaseHeightCells = 4;
|
||||
private const int InitialLoadDays = 3;
|
||||
private const int LoadMoreDays = 3;
|
||||
private const int MaxCachedDays = 30;
|
||||
|
||||
private readonly Dictionary<DateTime, JuyaDailyNews> _cachedNews = new();
|
||||
private readonly List<DateTime> _loadedDates = new();
|
||||
private readonly List<DailyNewsView> _dailyViews = new();
|
||||
|
||||
private double _currentCellSize = BaseCellSize;
|
||||
private bool _isAttached;
|
||||
private bool _isLoading;
|
||||
private bool _isNightVisual;
|
||||
private DateTime _earliestLoadedDate = DateTime.Today;
|
||||
|
||||
public JuyaNewsWidget()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
BrandTextBlock.FontFamily = MiSansFontFamily;
|
||||
LoadingTextBlock.FontFamily = MiSansFontFamily;
|
||||
StatusTextBlock.FontFamily = MiSansFontFamily;
|
||||
|
||||
AttachedToVisualTree += OnAttachedToVisualTree;
|
||||
DetachedFromVisualTree += OnDetachedFromVisualTree;
|
||||
SizeChanged += OnSizeChanged;
|
||||
ActualThemeVariantChanged += OnActualThemeVariantChanged;
|
||||
|
||||
ApplyCellSize(_currentCellSize);
|
||||
ApplyLoadingState();
|
||||
}
|
||||
|
||||
public void ApplyCellSize(double cellSize)
|
||||
{
|
||||
_currentCellSize = Math.Max(1, cellSize);
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private void OnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = true;
|
||||
_ = LoadInitialNewsAsync();
|
||||
}
|
||||
|
||||
private void OnDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
|
||||
{
|
||||
_isAttached = false;
|
||||
}
|
||||
|
||||
private void OnSizeChanged(object? sender, SizeChangedEventArgs e)
|
||||
{
|
||||
ApplyCellSize(_currentCellSize);
|
||||
}
|
||||
|
||||
private void OnActualThemeVariantChanged(object? sender, EventArgs e)
|
||||
{
|
||||
_isNightVisual = ResolveNightMode();
|
||||
UpdateAdaptiveLayout();
|
||||
}
|
||||
|
||||
private bool ResolveNightMode()
|
||||
{
|
||||
if (ActualThemeVariant == ThemeVariant.Dark)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ActualThemeVariant == ThemeVariant.Light)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.TryFindResource("AdaptiveSurfaceBaseBrush", out var value) &&
|
||||
value is ISolidColorBrush brush)
|
||||
{
|
||||
return CalculateRelativeLuminance(brush.Color) < 0.45;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static double CalculateRelativeLuminance(Color color)
|
||||
{
|
||||
static double ToLinear(double channel)
|
||||
{
|
||||
return channel <= 0.03928
|
||||
? channel / 12.92
|
||||
: Math.Pow((channel + 0.055) / 1.055, 2.4);
|
||||
}
|
||||
|
||||
var r = ToLinear(color.R / 255d);
|
||||
var g = ToLinear(color.G / 255d);
|
||||
var b = ToLinear(color.B / 255d);
|
||||
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
}
|
||||
|
||||
private void ApplyNightModeVisual()
|
||||
{
|
||||
// 卡片背景
|
||||
CardBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#2d2a2a") : Color.Parse("#fefefe"));
|
||||
|
||||
// 品牌标题
|
||||
BrandTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#d4736a") : Color.Parse("#bb5649"));
|
||||
|
||||
// 刷新按钮
|
||||
RefreshButton.BorderBrush = new SolidColorBrush(_isNightVisual ? Color.Parse("#d4736a") : Color.Parse("#bb5649"));
|
||||
RefreshButton.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#d4736a") : Color.Parse("#bb5649"));
|
||||
|
||||
// 头像背景
|
||||
AvatarBorder.Background = new SolidColorBrush(_isNightVisual ? Color.Parse("#3d3a3a") : Color.Parse("#f8f5ec"));
|
||||
|
||||
// 状态文字
|
||||
StatusTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#9a9590") : Color.Parse("#757575"));
|
||||
LoadingTextBlock.Foreground = new SolidColorBrush(_isNightVisual ? Color.Parse("#9a9590") : Color.Parse("#757575"));
|
||||
|
||||
// 更新所有日期视图的样式
|
||||
foreach (var view in _dailyViews)
|
||||
{
|
||||
view.ApplyNightMode(_isNightVisual);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadInitialNewsAsync()
|
||||
{
|
||||
if (!_isAttached || _isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
LoadingTextBlock.IsVisible = true;
|
||||
StatusTextBlock.IsVisible = false;
|
||||
|
||||
try
|
||||
{
|
||||
// 解析RSS获取所有新闻
|
||||
var allNews = await FetchJuyaNewsAsync();
|
||||
|
||||
if (!_isAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 缓存新闻数据
|
||||
foreach (var news in allNews)
|
||||
{
|
||||
_cachedNews[news.Date.Date] = news;
|
||||
}
|
||||
|
||||
// 加载最近几天的新闻
|
||||
var today = DateTime.Today;
|
||||
var datesToLoad = Enumerable.Range(0, InitialLoadDays)
|
||||
.Select(i => today.AddDays(-i))
|
||||
.Where(d => _cachedNews.ContainsKey(d))
|
||||
.OrderByDescending(d => d)
|
||||
.ToList();
|
||||
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
|
||||
NewsStackPanel.Children.Clear();
|
||||
_dailyViews.Clear();
|
||||
_loadedDates.Clear();
|
||||
|
||||
foreach (var date in datesToLoad)
|
||||
{
|
||||
AddDailyNewsToView(_cachedNews[date]);
|
||||
_loadedDates.Add(date);
|
||||
}
|
||||
|
||||
if (_loadedDates.Any())
|
||||
{
|
||||
_earliestLoadedDate = _loadedDates.Min();
|
||||
}
|
||||
|
||||
LoadingTextBlock.IsVisible = false;
|
||||
StatusTextBlock.IsVisible = false;
|
||||
UpdateAdaptiveLayout();
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
StatusTextBlock.Text = "加载失败";
|
||||
StatusTextBlock.IsVisible = true;
|
||||
LoadingTextBlock.IsVisible = false;
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<List<JuyaDailyNews>> FetchJuyaNewsAsync()
|
||||
{
|
||||
var result = new List<JuyaDailyNews>();
|
||||
|
||||
try
|
||||
{
|
||||
// 使用字节数组获取内容,确保正确解码 UTF-8
|
||||
var response = await HttpClient.GetByteArrayAsync(RssUrl);
|
||||
var rssContent = System.Text.Encoding.UTF8.GetString(response);
|
||||
var doc = XDocument.Parse(rssContent);
|
||||
|
||||
var contentNs = XNamespace.Get("http://purl.org/rss/1.0/modules/content/");
|
||||
|
||||
var items = doc.Descendants("item");
|
||||
|
||||
foreach (var item in items)
|
||||
{
|
||||
var title = item.Element("title")?.Value ?? "";
|
||||
var link = item.Element("link")?.Value ?? "";
|
||||
var pubDate = item.Element("pubDate")?.Value ?? "";
|
||||
var contentEncoded = item.Element(contentNs + "encoded")?.Value ?? "";
|
||||
|
||||
// 解析日期
|
||||
if (!DateTime.TryParse(pubDate, out var date))
|
||||
{
|
||||
date = DateTime.Today;
|
||||
}
|
||||
|
||||
// 提取封面图URL
|
||||
var coverImageUrl = ExtractCoverImageUrl(contentEncoded);
|
||||
|
||||
// 提取视频链接
|
||||
var (bilibiliUrl, youtubeUrl) = ExtractVideoUrls(contentEncoded);
|
||||
|
||||
// 解析概览(简短列表)
|
||||
var overviewCategories = ParseOverview(contentEncoded);
|
||||
|
||||
// 解析详细内容
|
||||
var detailedNews = ParseDetailedNews(contentEncoded);
|
||||
|
||||
var news = new JuyaDailyNews(
|
||||
Date: date,
|
||||
Title: title,
|
||||
CoverImageUrl: coverImageUrl,
|
||||
IssueUrl: link,
|
||||
BilibiliUrl: bilibiliUrl,
|
||||
YoutubeUrl: youtubeUrl,
|
||||
OverviewCategories: overviewCategories,
|
||||
DetailedNews: detailedNews,
|
||||
FetchedAt: DateTimeOffset.Now
|
||||
);
|
||||
|
||||
result.Add(news);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 返回空列表
|
||||
}
|
||||
|
||||
return result.OrderByDescending(n => n.Date).ToList();
|
||||
}
|
||||
|
||||
private static string ExtractCoverImageUrl(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
var match = Regex.Match(content, @"<img[^>]+src=[""']([^""']+)[""']", RegexOptions.IgnoreCase);
|
||||
return match.Success ? match.Groups[1].Value : "";
|
||||
}
|
||||
|
||||
private static (string bilibili, string youtube) ExtractVideoUrls(string content)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return ("", "");
|
||||
}
|
||||
|
||||
string bilibiliUrl = "";
|
||||
string youtubeUrl = "";
|
||||
|
||||
var bilibiliMatch = Regex.Match(content, @"<a[^>]+href=[""'](https?://(?:www\.)?bilibili\.com/[^""']+)[""'][^>]*>", RegexOptions.IgnoreCase);
|
||||
if (bilibiliMatch.Success)
|
||||
{
|
||||
bilibiliUrl = bilibiliMatch.Groups[1].Value;
|
||||
}
|
||||
|
||||
var youtubeMatch = Regex.Match(content, @"<a[^>]+href=[""'](https?://(?:www\.)?(?:youtube\.com|youtu\.be)/[^""']+)[""'][^>]*>", RegexOptions.IgnoreCase);
|
||||
if (youtubeMatch.Success)
|
||||
{
|
||||
youtubeUrl = youtubeMatch.Groups[1].Value;
|
||||
}
|
||||
|
||||
return (bilibiliUrl, youtubeUrl);
|
||||
}
|
||||
|
||||
private static List<JuyaOverviewCategory> ParseOverview(string content)
|
||||
{
|
||||
var categories = new List<JuyaOverviewCategory>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return categories;
|
||||
}
|
||||
|
||||
var categoryIcons = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
["要闻"] = "📌",
|
||||
["开发生态"] = "💻",
|
||||
["产品应用"] = "📱",
|
||||
["产品发布"] = "🚀",
|
||||
["模型发布"] = "🤖",
|
||||
["行业动态"] = "📈",
|
||||
["技术与洞察"] = "🔍",
|
||||
["学术研究"] = "📚",
|
||||
["研究"] = "🔬",
|
||||
["开源"] = "🔓",
|
||||
["投资"] = "💰",
|
||||
["融资"] = "💵",
|
||||
["商业"] = "💼",
|
||||
["市场"] = "📊",
|
||||
["AI绘画"] = "🎨",
|
||||
["设计"] = "✏️",
|
||||
["创意"] = "💡",
|
||||
["前瞻与传闻"] = "🔮",
|
||||
["趋势"] = "📉",
|
||||
["预测"] = "🔭",
|
||||
["政策"] = "📋",
|
||||
["法规"] = "⚖️",
|
||||
["监管"] = "🛡️",
|
||||
["硬件"] = "🔧",
|
||||
["芯片"] = "🖥️",
|
||||
["基础设施"] = "🏗️",
|
||||
["其他"] = "•",
|
||||
["要点"] = "📋",
|
||||
["摘要"] = "📝"
|
||||
};
|
||||
|
||||
var overviewMatch = Regex.Match(content, @"<h2>\s*概览\s*</h2>(.*?)(?:<hr>|$)", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
if (!overviewMatch.Success)
|
||||
{
|
||||
return categories;
|
||||
}
|
||||
|
||||
var overviewContent = overviewMatch.Groups[1].Value;
|
||||
|
||||
var h3Matches = Regex.Matches(overviewContent, @"<h3>([^<]+)</h3>\s*<ul>(.*?)</ul>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match match in h3Matches)
|
||||
{
|
||||
var categoryName = match.Groups[1].Value.Trim();
|
||||
var listContent = match.Groups[2].Value;
|
||||
|
||||
var icon = categoryIcons.GetValueOrDefault(categoryName, "•");
|
||||
|
||||
var items = new List<JuyaOverviewItem>();
|
||||
var itemMatches = Regex.Matches(listContent, @"<li>(.*?)</li>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match itemMatch in itemMatches)
|
||||
{
|
||||
var itemText = itemMatch.Groups[1].Value;
|
||||
|
||||
string itemTitle;
|
||||
string itemUrl;
|
||||
int? number = null;
|
||||
|
||||
var linkMatch = Regex.Match(itemText, @"<a[^>]+href=[""']([^""']+)[""'][^>]*>(.*?)</a>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
if (linkMatch.Success)
|
||||
{
|
||||
itemUrl = linkMatch.Groups[1].Value;
|
||||
var linkText = Regex.Replace(linkMatch.Groups[2].Value, @"<[^>]+>", "").Trim();
|
||||
|
||||
var beforeLink = itemText.Substring(0, itemText.IndexOf("<a", StringComparison.OrdinalIgnoreCase));
|
||||
itemTitle = Regex.Replace(beforeLink, @"<[^>]+>", "").Trim();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(itemTitle))
|
||||
{
|
||||
itemTitle = linkText;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
itemTitle = Regex.Replace(itemText, @"<[^>]+>", "").Trim();
|
||||
itemUrl = "";
|
||||
}
|
||||
|
||||
var numberMatch = Regex.Match(itemText, @"<code>\s*#(\d+)\s*</code>|#(\d+)");
|
||||
if (numberMatch.Success)
|
||||
{
|
||||
number = int.Parse(numberMatch.Groups[1].Success ? numberMatch.Groups[1].Value : numberMatch.Groups[2].Value);
|
||||
}
|
||||
|
||||
itemTitle = Regex.Replace(itemTitle, @"^\s*#\d+\s*", "").Trim();
|
||||
itemTitle = Regex.Replace(itemTitle, @"[→↗\s]+$", "").Trim();
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(itemTitle) && itemTitle.Length > 1)
|
||||
{
|
||||
items.Add(new JuyaOverviewItem(itemTitle, itemUrl, number));
|
||||
}
|
||||
}
|
||||
|
||||
if (items.Any())
|
||||
{
|
||||
categories.Add(new JuyaOverviewCategory(categoryName, icon, items));
|
||||
}
|
||||
}
|
||||
|
||||
return categories;
|
||||
}
|
||||
|
||||
private static List<JuyaDetailedNewsItem> ParseDetailedNews(string content)
|
||||
{
|
||||
var newsItems = new List<JuyaDetailedNewsItem>();
|
||||
|
||||
if (string.IsNullOrWhiteSpace(content))
|
||||
{
|
||||
return newsItems;
|
||||
}
|
||||
|
||||
var detailedMatch = Regex.Match(content, @"<hr>(.*)$", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
if (!detailedMatch.Success)
|
||||
{
|
||||
return newsItems;
|
||||
}
|
||||
|
||||
var detailedContent = detailedMatch.Groups[1].Value;
|
||||
|
||||
var newsMatches = Regex.Matches(detailedContent, @"<h2>(.*?)</h2>(.*?)(?=<h2>|<hr>|$)", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
|
||||
foreach (Match match in newsMatches)
|
||||
{
|
||||
var headerContent = match.Groups[1].Value;
|
||||
var bodyContent = match.Groups[2].Value;
|
||||
|
||||
var numberMatch = Regex.Match(headerContent, @"<code>\s*#(\d+)\s*</code>");
|
||||
if (!numberMatch.Success)
|
||||
{
|
||||
numberMatch = Regex.Match(headerContent, @"#(\d+)");
|
||||
}
|
||||
|
||||
int? number = numberMatch.Success ? int.Parse(numberMatch.Groups[1].Value) : null;
|
||||
|
||||
string title;
|
||||
var linkMatch = Regex.Match(headerContent, @"<a[^>]*>(.*?)</a>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
if (linkMatch.Success)
|
||||
{
|
||||
title = Regex.Replace(linkMatch.Groups[1].Value, @"<[^>]+>", "").Trim();
|
||||
}
|
||||
else
|
||||
{
|
||||
title = Regex.Replace(headerContent, @"<code>.*?</code>", "", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
title = Regex.Replace(title, @"<[^>]+>", "").Trim();
|
||||
title = Regex.Replace(title, @"#\d+", "").Trim();
|
||||
}
|
||||
|
||||
var bodyText = ExtractBodyText(bodyContent);
|
||||
|
||||
var relatedLinks = new List<string>();
|
||||
var linkMatches = Regex.Matches(bodyContent, @"<a[^>]+href=[""']([^""']+)[""'][^>]*>", RegexOptions.IgnoreCase);
|
||||
foreach (Match linkMatch2 in linkMatches)
|
||||
{
|
||||
var url = linkMatch2.Groups[1].Value;
|
||||
if (!string.IsNullOrWhiteSpace(url) && !relatedLinks.Contains(url))
|
||||
{
|
||||
relatedLinks.Add(url);
|
||||
}
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(title) && !string.IsNullOrWhiteSpace(bodyText))
|
||||
{
|
||||
newsItems.Add(new JuyaDetailedNewsItem(title, number ?? 0, bodyText, relatedLinks));
|
||||
}
|
||||
}
|
||||
|
||||
return newsItems;
|
||||
}
|
||||
|
||||
private static string ExtractBodyText(string htmlContent)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(htmlContent))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// 提取 blockquote 内容
|
||||
var blockquoteMatch = Regex.Match(htmlContent, @"<blockquote>(.*?)</blockquote>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
if (blockquoteMatch.Success)
|
||||
{
|
||||
var text = blockquoteMatch.Groups[1].Value;
|
||||
// 移除 <p> 标签但保留内容
|
||||
text = Regex.Replace(text, @"<p>(.*?)</p>", "$1\n\n", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
// 移除其他 HTML 标签
|
||||
text = Regex.Replace(text, @"<[^>]+>", "");
|
||||
// 清理多余空白
|
||||
text = Regex.Replace(text, @"\n{3,}", "\n\n");
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
// 如果没有 blockquote,提取所有 <p> 标签内容
|
||||
var paragraphs = Regex.Matches(htmlContent, @"<p>(.*?)</p>", RegexOptions.Singleline | RegexOptions.IgnoreCase);
|
||||
if (paragraphs.Count > 0)
|
||||
{
|
||||
var text = string.Join("\n\n", paragraphs.Cast<Match>().Select(m =>
|
||||
Regex.Replace(m.Groups[1].Value, @"<[^>]+>", "").Trim()));
|
||||
return text.Trim();
|
||||
}
|
||||
|
||||
// 最后尝试直接移除所有 HTML 标签
|
||||
return Regex.Replace(htmlContent, @"<[^>]+>", "").Trim();
|
||||
}
|
||||
|
||||
private void AddDailyNewsToView(JuyaDailyNews news)
|
||||
{
|
||||
var view = new DailyNewsView(news, _isNightVisual);
|
||||
view.CoverImageClicked += (s, e) => TryOpenUrl(news.IssueUrl);
|
||||
view.NewsItemClicked += (s, url) => TryOpenUrl(url);
|
||||
NewsStackPanel.Children.Add(view);
|
||||
_dailyViews.Add(view);
|
||||
}
|
||||
|
||||
private async void OnScrollChanged(object? sender, ScrollChangedEventArgs e)
|
||||
{
|
||||
if (_isLoading || !_isAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var scrollViewer = (ScrollViewer)sender!;
|
||||
|
||||
var offset = scrollViewer.Offset;
|
||||
var extent = scrollViewer.Extent;
|
||||
var viewport = scrollViewer.Viewport;
|
||||
|
||||
if (offset.Y >= extent.Height - viewport.Height - 200)
|
||||
{
|
||||
await LoadMoreNewsAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private async Task LoadMoreNewsAsync()
|
||||
{
|
||||
if (_isLoading || !_isAttached)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var nextDates = Enumerable.Range(1, LoadMoreDays)
|
||||
.Select(i => _earliestLoadedDate.AddDays(-i))
|
||||
.Where(d => _cachedNews.ContainsKey(d) && !_loadedDates.Contains(d))
|
||||
.ToList();
|
||||
|
||||
if (!nextDates.Any())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isLoading = true;
|
||||
LoadingTextBlock.IsVisible = true;
|
||||
|
||||
try
|
||||
{
|
||||
await Dispatcher.UIThread.InvokeAsync(() =>
|
||||
{
|
||||
if (!_isAttached) return;
|
||||
|
||||
foreach (var date in nextDates.OrderByDescending(d => d))
|
||||
{
|
||||
AddDailyNewsToView(_cachedNews[date]);
|
||||
_loadedDates.Add(date);
|
||||
}
|
||||
|
||||
_earliestLoadedDate = _loadedDates.Min();
|
||||
LoadingTextBlock.IsVisible = false;
|
||||
UpdateAdaptiveLayout();
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnRefreshButtonClick(object? sender, RoutedEventArgs e)
|
||||
{
|
||||
e.Handled = true;
|
||||
|
||||
if (_isLoading)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedNews.Clear();
|
||||
_loadedDates.Clear();
|
||||
_dailyViews.Clear();
|
||||
NewsStackPanel.Children.Clear();
|
||||
_earliestLoadedDate = DateTime.Today;
|
||||
|
||||
await LoadInitialNewsAsync();
|
||||
}
|
||||
|
||||
private void TryOpenUrl(string? url)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = url,
|
||||
UseShellExecute = true
|
||||
};
|
||||
Process.Start(startInfo);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// 忽略错误
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyLoadingState()
|
||||
{
|
||||
StatusTextBlock.Text = "加载中...";
|
||||
StatusTextBlock.IsVisible = true;
|
||||
}
|
||||
|
||||
private void UpdateAdaptiveLayout()
|
||||
{
|
||||
var scale = ResolveScale();
|
||||
var softScale = Math.Clamp(scale, 0.80, 1.32);
|
||||
var totalWidth = Bounds.Width > 1 ? Bounds.Width : _currentCellSize * BaseWidthCells;
|
||||
var totalHeight = Bounds.Height > 1 ? Bounds.Height : _currentCellSize * BaseHeightCells;
|
||||
|
||||
var unifiedMainRectangle = ResolveUnifiedMainRectangle();
|
||||
RootBorder.CornerRadius = unifiedMainRectangle;
|
||||
CardBorder.CornerRadius = unifiedMainRectangle;
|
||||
|
||||
var horizontalPadding = Math.Clamp(16 * softScale, 10, 24);
|
||||
var verticalPadding = Math.Clamp(14 * softScale, 8, 20);
|
||||
CardBorder.Padding = new Thickness(horizontalPadding, verticalPadding, horizontalPadding, verticalPadding);
|
||||
|
||||
var headerHeight = Math.Clamp(40 * softScale, 28, 56);
|
||||
HeaderGrid.Height = headerHeight;
|
||||
|
||||
BrandTextBlock.FontSize = Math.Clamp(20 * softScale, 14, 26);
|
||||
|
||||
var avatarSize = Math.Clamp(36 * softScale, 24, 48);
|
||||
AvatarBorder.Width = avatarSize;
|
||||
AvatarBorder.Height = avatarSize;
|
||||
AvatarBorder.CornerRadius = new CornerRadius(avatarSize / 2);
|
||||
|
||||
var buttonFontSize = Math.Clamp(13 * softScale, 10, 16);
|
||||
RefreshButton.FontSize = buttonFontSize;
|
||||
RefreshButton.Padding = new Thickness(
|
||||
Math.Clamp(8 * softScale, 6, 12),
|
||||
Math.Clamp(4 * softScale, 2, 6)
|
||||
);
|
||||
|
||||
StatusTextBlock.FontSize = Math.Clamp(16 * softScale, 12, 22);
|
||||
LoadingTextBlock.FontSize = Math.Clamp(14 * softScale, 11, 18);
|
||||
|
||||
foreach (var view in _dailyViews)
|
||||
{
|
||||
view.UpdateLayout(softScale, totalWidth - horizontalPadding * 2);
|
||||
}
|
||||
|
||||
ApplyNightModeVisual();
|
||||
}
|
||||
|
||||
private double ResolveScale()
|
||||
{
|
||||
var expectedWidth = _currentCellSize * BaseWidthCells;
|
||||
var expectedHeight = _currentCellSize * BaseHeightCells;
|
||||
if (expectedWidth <= 0 || expectedHeight <= 0)
|
||||
{
|
||||
return 1d;
|
||||
}
|
||||
|
||||
var actualWidth = Bounds.Width > 1 ? Bounds.Width : expectedWidth;
|
||||
var actualHeight = Bounds.Height > 1 ? Bounds.Height : expectedHeight;
|
||||
var scaleX = actualWidth / expectedWidth;
|
||||
var scaleY = actualHeight / expectedHeight;
|
||||
return Math.Clamp(Math.Min(scaleX, scaleY), 0.72, 2.4);
|
||||
}
|
||||
|
||||
private CornerRadius ResolveUnifiedMainRectangle() => new(ResolveUnifiedMainRadiusValue());
|
||||
|
||||
private static double ResolveUnifiedMainRadiusValue() =>
|
||||
HostAppearanceThemeProvider.GetOrCreate().GetCurrent().CornerRadiusTokens.Lg.TopLeft;
|
||||
}
|
||||
|
||||
// 数据模型
|
||||
public sealed record JuyaDailyNews(
|
||||
DateTime Date,
|
||||
string Title,
|
||||
string CoverImageUrl,
|
||||
string IssueUrl,
|
||||
string BilibiliUrl,
|
||||
string YoutubeUrl,
|
||||
IReadOnlyList<JuyaOverviewCategory> OverviewCategories,
|
||||
IReadOnlyList<JuyaDetailedNewsItem> DetailedNews,
|
||||
DateTimeOffset FetchedAt);
|
||||
|
||||
public sealed record JuyaOverviewCategory(
|
||||
string Name,
|
||||
string Icon,
|
||||
IReadOnlyList<JuyaOverviewItem> Items);
|
||||
|
||||
public sealed record JuyaOverviewItem(
|
||||
string Title,
|
||||
string Url,
|
||||
int? Number);
|
||||
|
||||
public sealed record JuyaDetailedNewsItem(
|
||||
string Title,
|
||||
int Number,
|
||||
string BodyText,
|
||||
IReadOnlyList<string> RelatedLinks);
|
||||
Reference in New Issue
Block a user