using System.Text.Json;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.Web.WebView2.Core;
using Windows.UI;
using XFEEDToolBox.Core.News;
namespace XFEEDToolBox.Controls;
public sealed partial class GalnetNewsPanel : UserControl
{
private const string LocalOrigin = "https://galnet.toolbox.invalid/";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly GalnetNewsService news;
private readonly GalnetMediaCatalog media;
private readonly GalnetNewsTranslator translator;
private readonly DispatcherQueueTimer timer;
private readonly List<GalnetSlide> slides = [];
private readonly HashSet<(int Generation, string Id)> previewJobs = [];
private CancellationTokenSource? refresh, presentation;
private GalnetNewsSnapshot? displayed;
private DateTimeOffset nextRefresh;
private bool active, browserReady, browserInitializing, dialogOpen;
private int presentationGeneration;
private WebView2? articleBrowser;
private string? articleId;
public GalnetNewsPanel()
{
InitializeComponent();
news = new(Path.Combine(App.Runtime.DataDirectory, "galnet-news.json"));
media = new(Path.Combine(App.Runtime.DataDirectory, "galnet-media.json"));
translator = new(Path.Combine(App.Runtime.DataDirectory, "galnet-translations.json"));
timer = DispatcherQueue.CreateTimer();
timer.Interval = TimeSpan.FromMinutes(1);
timer.Tick += (_, _) => { if (DateTimeOffset.UtcNow >= nextRefresh) _ = RefreshAsync(); };
Loaded += (_, _) => ResumePage();
Unloaded += (_, _) => SuspendPage();
}
public void ResumePage()
{
if (active || !IsLoaded) return;
active = true;
// Existing interface probes must remain offline and cannot start external requests.
if (Environment.GetCommandLineArgs().Any(arg => arg.StartsWith("--smoke-", StringComparison.Ordinal)
|| arg.StartsWith("--accept-", StringComparison.Ordinal) || arg == "--check-navigation")) return;
_ = InitializeCarouselAsync();
timer.Start();
_ = SetCarouselActiveAsync(true);
if (DateTimeOffset.UtcNow >= nextRefresh || presentation?.IsCancellationRequested != false) _ = RefreshAsync();
}
public void SuspendPage()
{
active = false;
timer.Stop();
refresh?.Cancel(); presentation?.Cancel();
_ = SetCarouselActiveAsync(false);
}
private async Task InitializeCarouselAsync()
{
if (browserReady || browserInitializing) return;
browserInitializing = true;
try { await InitializeBrowserAsync(NewsBrowser, false); }
catch (Exception error) { App.Runtime.Log("银河新闻界面", error.Message); NewsStatus.Text = "新闻界面暂时无法载入,请刷新重试。"; }
finally { browserInitializing = false; }
}
private async Task InitializeBrowserAsync(WebView2 browser, bool article)
{
var environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, Path.Combine(App.Runtime.DataDirectory, "webview-galnet"), null);
await browser.EnsureCoreWebView2Async(environment);
var core = browser.CoreWebView2;
core.SetVirtualHostNameToFolderMapping("galnet.toolbox.invalid", Path.Combine(AppContext.BaseDirectory, "Assets", "Galnet"), CoreWebView2HostResourceAccessKind.DenyCors);
core.Settings.IsStatusBarEnabled = false;
core.Settings.IsZoomControlEnabled = false;
core.NavigationStarting += (_, args) => { if (!args.Uri.StartsWith(LocalOrigin, StringComparison.Ordinal)) args.Cancel = true; };
core.NewWindowRequested += (_, args) => args.Handled = true;
core.WebMessageReceived += async (_, args) =>
{
if (!args.Source.StartsWith(LocalOrigin, StringComparison.Ordinal)) return;
try
{
using var json = JsonDocument.Parse(args.WebMessageAsJson);
var root = json.RootElement;
var type = root.GetProperty("type").GetString();
var id = root.TryGetProperty("id", out var idValue) ? idValue.GetString() : null;
if (type == "ready")
{
if (!article) { browserReady = true; await PushSlidesAsync(); await SetCarouselActiveAsync(active && !dialogOpen); }
else await PushArticleAsync();
}
else if (id is not null && slides.Any(s => s.Id == id))
{
if (type == "select") QueuePreviews(id);
else if (type == "open" && !article) await OpenArticleAsync(id);
else if (type == "official" && article && slides.First(s => s.Id == id).Article.SourceUri is { } source)
await Windows.System.Launcher.LaunchUriAsync(source);
}
}
catch (Exception error) { App.Runtime.Log("银河新闻界面", error.Message); }
};
core.Navigate(LocalOrigin + "index.html");
}
private async void RefreshClicked(object sender, RoutedEventArgs args)
{
if (refresh is not null) { refresh.Cancel(); return; }
await InitializeCarouselAsync();
await RefreshAsync();
}
private async Task RefreshAsync()
{
if (!active || refresh is not null) return;
using var operation = new CancellationTokenSource();
refresh = operation; SetBusy(true);
nextRefresh = DateTimeOffset.UtcNow.AddMinutes(1);
try
{
var saved = await news.LoadCachedAsync(operation.Token);
ShowEdition(saved);
NewsStatus.Text = saved is null ? "正在获取银河新闻…" : "正在检查新新闻,已保存的内容仍可阅读。";
var result = await news.RefreshAsync(operation.Token);
operation.Token.ThrowIfCancellationRequested();
ShowEdition(result.Snapshot);
NewsStatus.Text = result.Message + " · 自动翻译为中文 · 鼠标停留时暂停轮播";
if (!result.Message.StartsWith("暂时", StringComparison.Ordinal)) nextRefresh = DateTimeOffset.UtcNow.AddMinutes(10);
}
catch (OperationCanceledException)
{
NewsStatus.Text = displayed is null ? "已取消获取新闻。" : "已取消刷新,继续显示已保存的内容。";
nextRefresh = DateTimeOffset.UtcNow.AddMinutes(1);
}
catch (Exception error)
{
App.Runtime.Log("银河新闻", error.Message);
NewsStatus.Text = displayed is null ? "暂时无法获取银河新闻,稍后会自动重试。" : "暂时无法获取新新闻,继续显示已保存的内容。";
}
finally { refresh = null; SetBusy(false); if (active && operation.IsCancellationRequested) nextRefresh = DateTimeOffset.UtcNow; }
}
private void ShowEdition(GalnetNewsSnapshot? snapshot)
{
if (snapshot is null || ReferenceEquals(displayed, snapshot) && presentation?.IsCancellationRequested == false) return;
displayed = snapshot;
presentation?.Cancel(); presentation?.Dispose(); presentation = new();
var generation = ++presentationGeneration;
EditionText.Text = $"Galnet · Frontier 官方新闻 · {snapshot.RetrievedAt.ToLocalTime():MM-dd HH:mm} 更新";
_ = PrepareSlidesAsync(snapshot, generation, presentation.Token);
}
private async Task PrepareSlidesAsync(GalnetNewsSnapshot snapshot, int generation, CancellationToken token)
{
try
{
await media.LoadAsync(token);
var prepared = new List<GalnetSlide>();
foreach (var article in snapshot.Articles.Take(6))
{
var preview = GalnetSlide.Preview(article.Body);
var title = await translator.CachedAsync(article.Title, token);
var teaser = await translator.CachedAsync(preview, token);
var body = await translator.CachedAsync(article.Body, token);
prepared.Add(new(article, media.Image(article), title ?? article.Title, teaser ?? preview, body ?? article.Body, title is not null && teaser is not null, body is not null));
}
token.ThrowIfCancellationRequested(); if (generation != presentationGeneration) return;
slides.Clear(); slides.AddRange(prepared); await PushSlidesAsync();
if (slides.Count > 0) QueuePreviews(slides[0].Id);
try
{
await media.RefreshAsync(snapshot, token);
token.ThrowIfCancellationRequested(); if (generation != presentationGeneration) return;
for (var i = 0; i < slides.Count; i++) slides[i] = slides[i] with { Image = media.Image(slides[i].Article) };
await PushSlidesAsync(); await PushArticleAsync();
}
catch (Exception error) when (error is not OperationCanceledException) { App.Runtime.Log("银河新闻配图", error.Message); }
}
catch (OperationCanceledException) { }
catch (Exception error) { App.Runtime.Log("银河新闻展示", error.Message); }
}
private void QueuePreviews(string id)
{
if (!active || presentation is null || presentation.IsCancellationRequested) return;
var index = slides.FindIndex(s => s.Id == id); if (index < 0) return;
_ = TranslatePreviewAsync(id, presentationGeneration, presentation.Token);
if (slides.Count > 1) _ = TranslatePreviewAsync(slides[(index + 1) % slides.Count].Id, presentationGeneration, presentation.Token);
}
private async Task TranslatePreviewAsync(string id, int generation, CancellationToken token)
{
if (!previewJobs.Add((generation, id))) return;
try
{
var article = slides.FirstOrDefault(s => s.Id == id)?.Article; if (article is null) return;
var title = await translator.TranslateAsync(article.Title, token);
await UpdateSlideAsync(id, generation, s => s with { Title = title });
var teaser = await translator.TranslateAsync(GalnetSlide.Preview(article.Body), token);
await UpdateSlideAsync(id, generation, s => s with { Teaser = teaser, PreviewTranslated = true, TranslationStatus = "中文 · 自动翻译" });
}
catch (OperationCanceledException) { }
catch (Exception error)
{
App.Runtime.Log("银河新闻翻译", error.Message);
await UpdateSlideAsync(id, generation, s => s with { TranslationStatus = "翻译暂不可用 · 可阅读原文" });
}
finally { previewJobs.Remove((generation, id)); }
}
private async Task UpdateSlideAsync(string id, int generation, Func<GalnetSlide, GalnetSlide> update)
{
if (generation != presentationGeneration || !active) return;
var index = slides.FindIndex(s => s.Id == id); if (index < 0) return;
slides[index] = update(slides[index]); await PushSlidesAsync(); if (articleId == id) await PushArticleAsync();
}
private async Task PushSlidesAsync()
{
if (browserReady) await NewsBrowser.ExecuteScriptAsync("window.galnet?.setSlides(" + JsonSerializer.Serialize(slides, JsonOptions) + ")");
}
private async Task SetCarouselActiveAsync(bool value)
{
try { if (browserReady) await NewsBrowser.ExecuteScriptAsync("window.galnet?.setActive(" + (value ? "true" : "false") + ")"); }
catch (Exception error) { App.Runtime.Log("银河新闻轮播", error.Message); }
}
private async Task PushArticleAsync()
{
if (articleBrowser?.CoreWebView2 is null || slides.FirstOrDefault(s => s.Id == articleId) is not { } slide) return;
await articleBrowser.ExecuteScriptAsync("window.galnet?.setArticle(" + JsonSerializer.Serialize(slide, JsonOptions) + ")");
}
private async Task TranslateBodyAsync(string id, int generation, CancellationToken token)
{
try
{
var slide = slides.First(s => s.Id == id);
var title = await translator.TranslateAsync(slide.Article.Title, token);
var body = await translator.TranslateAsync(slide.Article.Body, token);
await UpdateSlideAsync(id, generation, s => s with { Title = title, Body = body, BodyTranslated = true, TranslationStatus = "中文 · 自动翻译" });
}
catch (OperationCanceledException) { }
catch (Exception error)
{
App.Runtime.Log("银河新闻全文翻译", error.Message);
await UpdateSlideAsync(id, generation, s => s with { TranslationStatus = "翻译暂不可用 · 可阅读原文" });
}
}
private async Task OpenArticleAsync(string id)
{
if (dialogOpen || !active || presentation is null) return;
dialogOpen = true; articleId = id;
await SetCarouselActiveAsync(false);
var readerRoot = XamlRoot;
var readerWidth = Math.Clamp(readerRoot.Size.Width - 100, 320, 1280);
// WebView2 has no useful intrinsic desired width. Give the dialog's
// auto-sized content a real width instead of its 320px minimum layout.
var browser = new WebView2 { DefaultBackgroundColor = Color.FromArgb(255, 3, 3, 5), Width = readerWidth - 48,
Height = Math.Clamp(readerRoot.Size.Height - 200, 260, 900), HorizontalAlignment = HorizontalAlignment.Stretch };
var dialog = new ContentDialog { Title = "GALNET · 银河新闻", CloseButtonText = "关闭", Content = browser, XamlRoot = readerRoot,
Width = readerWidth, MaxWidth = 1280, HorizontalContentAlignment = HorizontalAlignment.Stretch, Background = new SolidColorBrush(Color.FromArgb(255, 3, 3, 5)) };
dialog.Resources["ContentDialogMaxWidth"] = 1280d;
void ResizeReader(XamlRoot root, XamlRootChangedEventArgs args)
{
dialog.Width = Math.Clamp(root.Size.Width - 100, 320, 1280);
browser.Width = dialog.Width - 48; browser.Height = Math.Clamp(root.Size.Height - 200, 260, 900);
}
readerRoot.Changed += ResizeReader;
articleBrowser = browser;
dialog.Opened += async (_, _) =>
{
try
{
await InitializeBrowserAsync(browser, true);
_ = TranslateBodyAsync(id, presentationGeneration, presentation.Token);
}
catch (Exception error) { App.Runtime.Log("银河新闻阅读", error.Message); }
};
try { await dialog.ShowAsync(); }
finally { readerRoot.Changed -= ResizeReader; articleBrowser = null; articleId = null; browser.Close(); dialogOpen = false; await SetCarouselActiveAsync(active); }
}
private void SetBusy(bool busy)
{
RefreshText.Text = busy ? "取消" : "刷新";
RefreshIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
RefreshProgress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
RefreshProgress.IsActive = busy;
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(RefreshButton, busy ? "取消刷新银河新闻" : "刷新银河新闻");
}
}
using System.Text.Json;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Media;
using Microsoft.Web.WebView2.Core;
using Windows.UI;
using XFEEDToolBox.Core.News;
namespace XFEEDToolBox.Controls;
public sealed partial class GalnetNewsPanel : UserControl
{
private const string LocalOrigin = "https://galnet.toolbox.invalid/";
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly GalnetNewsService news;
private readonly GalnetMediaCatalog media;
private readonly GalnetNewsTranslator translator;
private readonly DispatcherQueueTimer timer;
private readonly List<GalnetSlide> slides = [];
private readonly HashSet<(int Generation, string Id)> previewJobs = [];
private CancellationTokenSource? refresh, presentation;
private GalnetNewsSnapshot? displayed;
private DateTimeOffset nextRefresh;
private bool active, browserReady, browserInitializing, dialogOpen;
private int presentationGeneration;
private WebView2? articleBrowser;
private string? articleId;
public GalnetNewsPanel()
{
InitializeComponent();
news = new(Path.Combine(App.Runtime.DataDirectory, "galnet-news.json"));
media = new(Path.Combine(App.Runtime.DataDirectory, "galnet-media.json"));
translator = new(Path.Combine(App.Runtime.DataDirectory, "galnet-translations.json"));
timer = DispatcherQueue.CreateTimer();
timer.Interval = TimeSpan.FromMinutes(1);
timer.Tick += (_, _) => { if (DateTimeOffset.UtcNow >= nextRefresh) _ = RefreshAsync(); };
Loaded += (_, _) => ResumePage();
Unloaded += (_, _) => SuspendPage();
}
public void ResumePage()
{
if (active || !IsLoaded) return;
active = true;
// Existing interface probes must remain offline and cannot start external requests.
if (Environment.GetCommandLineArgs().Any(arg => arg.StartsWith("--smoke-", StringComparison.Ordinal)
|| arg.StartsWith("--accept-", StringComparison.Ordinal) || arg == "--check-navigation")) return;
_ = InitializeCarouselAsync();
timer.Start();
_ = SetCarouselActiveAsync(true);
if (DateTimeOffset.UtcNow >= nextRefresh || presentation?.IsCancellationRequested != false) _ = RefreshAsync();
}
public void SuspendPage()
{
active = false;
timer.Stop();
refresh?.Cancel(); presentation?.Cancel();
_ = SetCarouselActiveAsync(false);
}
private async Task InitializeCarouselAsync()
{
if (browserReady || browserInitializing) return;
browserInitializing = true;
try { await InitializeBrowserAsync(NewsBrowser, false); }
catch (Exception error) { App.Runtime.Log("银河新闻界面", error.Message); NewsStatus.Text = "新闻界面暂时无法载入,请刷新重试。"; }
finally { browserInitializing = false; }
}
private async Task InitializeBrowserAsync(WebView2 browser, bool article)
{
var environment = await CoreWebView2Environment.CreateWithOptionsAsync(null, Path.Combine(App.Runtime.DataDirectory, "webview-galnet"), null);
await browser.EnsureCoreWebView2Async(environment);
var core = browser.CoreWebView2;
core.SetVirtualHostNameToFolderMapping("galnet.toolbox.invalid", Path.Combine(AppContext.BaseDirectory, "Assets", "Galnet"), CoreWebView2HostResourceAccessKind.DenyCors);
core.Settings.IsStatusBarEnabled = false;
core.Settings.IsZoomControlEnabled = false;
core.NavigationStarting += (_, args) => { if (!args.Uri.StartsWith(LocalOrigin, StringComparison.Ordinal)) args.Cancel = true; };
core.NewWindowRequested += (_, args) => args.Handled = true;
core.WebMessageReceived += async (_, args) =>
{
if (!args.Source.StartsWith(LocalOrigin, StringComparison.Ordinal)) return;
try
{
using var json = JsonDocument.Parse(args.WebMessageAsJson);
var root = json.RootElement;
var type = root.GetProperty("type").GetString();
var id = root.TryGetProperty("id", out var idValue) ? idValue.GetString() : null;
if (type == "ready")
{
if (!article) { browserReady = true; await PushSlidesAsync(); await SetCarouselActiveAsync(active && !dialogOpen); }
else await PushArticleAsync();
}
else if (id is not null && slides.Any(s => s.Id == id))
{
if (type == "select") QueuePreviews(id);
else if (type == "open" && !article) await OpenArticleAsync(id);
else if (type == "official" && article && slides.First(s => s.Id == id).Article.SourceUri is { } source)
await Windows.System.Launcher.LaunchUriAsync(source);
}
}
catch (Exception error) { App.Runtime.Log("银河新闻界面", error.Message); }
};
core.Navigate(LocalOrigin + "index.html");
}
private async void RefreshClicked(object sender, RoutedEventArgs args)
{
if (refresh is not null) { refresh.Cancel(); return; }
await InitializeCarouselAsync();
await RefreshAsync();
}
private async Task RefreshAsync()
{
if (!active || refresh is not null) return;
using var operation = new CancellationTokenSource();
refresh = operation; SetBusy(true);
nextRefresh = DateTimeOffset.UtcNow.AddMinutes(1);
try
{
var saved = await news.LoadCachedAsync(operation.Token);
ShowEdition(saved);
NewsStatus.Text = saved is null ? "正在获取银河新闻…" : "正在检查新新闻,已保存的内容仍可阅读。";
var result = await news.RefreshAsync(operation.Token);
operation.Token.ThrowIfCancellationRequested();
ShowEdition(result.Snapshot);
NewsStatus.Text = result.Message + " · 自动翻译为中文 · 鼠标停留时暂停轮播";
if (!result.Message.StartsWith("暂时", StringComparison.Ordinal)) nextRefresh = DateTimeOffset.UtcNow.AddMinutes(10);
}
catch (OperationCanceledException)
{
NewsStatus.Text = displayed is null ? "已取消获取新闻。" : "已取消刷新,继续显示已保存的内容。";
nextRefresh = DateTimeOffset.UtcNow.AddMinutes(1);
}
catch (Exception error)
{
App.Runtime.Log("银河新闻", error.Message);
NewsStatus.Text = displayed is null ? "暂时无法获取银河新闻,稍后会自动重试。" : "暂时无法获取新新闻,继续显示已保存的内容。";
}
finally { refresh = null; SetBusy(false); if (active && operation.IsCancellationRequested) nextRefresh = DateTimeOffset.UtcNow; }
}
private void ShowEdition(GalnetNewsSnapshot? snapshot)
{
if (snapshot is null || ReferenceEquals(displayed, snapshot) && presentation?.IsCancellationRequested == false) return;
displayed = snapshot;
presentation?.Cancel(); presentation?.Dispose(); presentation = new();
var generation = ++presentationGeneration;
EditionText.Text = $"Galnet · Frontier 官方新闻 · {snapshot.RetrievedAt.ToLocalTime():MM-dd HH:mm} 更新";
_ = PrepareSlidesAsync(snapshot, generation, presentation.Token);
}
private async Task PrepareSlidesAsync(GalnetNewsSnapshot snapshot, int generation, CancellationToken token)
{
try
{
await media.LoadAsync(token);
var prepared = new List<GalnetSlide>();
foreach (var article in snapshot.Articles.Take(6))
{
var preview = GalnetSlide.Preview(article.Body);
var title = await translator.CachedAsync(article.Title, token);
var teaser = await translator.CachedAsync(preview, token);
var body = await translator.CachedAsync(article.Body, token);
prepared.Add(new(article, media.Image(article), title ?? article.Title, teaser ?? preview, body ?? article.Body, title is not null && teaser is not null, body is not null));
}
token.ThrowIfCancellationRequested(); if (generation != presentationGeneration) return;
slides.Clear(); slides.AddRange(prepared); await PushSlidesAsync();
if (slides.Count > 0) QueuePreviews(slides[0].Id);
try
{
await media.RefreshAsync(snapshot, token);
token.ThrowIfCancellationRequested(); if (generation != presentationGeneration) return;
for (var i = 0; i < slides.Count; i++) slides[i] = slides[i] with { Image = media.Image(slides[i].Article) };
await PushSlidesAsync(); await PushArticleAsync();
}
catch (Exception error) when (error is not OperationCanceledException) { App.Runtime.Log("银河新闻配图", error.Message); }
}
catch (OperationCanceledException) { }
catch (Exception error) { App.Runtime.Log("银河新闻展示", error.Message); }
}
private void QueuePreviews(string id)
{
if (!active || presentation is null || presentation.IsCancellationRequested) return;
var index = slides.FindIndex(s => s.Id == id); if (index < 0) return;
_ = TranslatePreviewAsync(id, presentationGeneration, presentation.Token);
if (slides.Count > 1) _ = TranslatePreviewAsync(slides[(index + 1) % slides.Count].Id, presentationGeneration, presentation.Token);
}
private async Task TranslatePreviewAsync(string id, int generation, CancellationToken token)
{
if (!previewJobs.Add((generation, id))) return;
try
{
var article = slides.FirstOrDefault(s => s.Id == id)?.Article; if (article is null) return;
var title = await translator.TranslateAsync(article.Title, token);
await UpdateSlideAsync(id, generation, s => s with { Title = title });
var teaser = await translator.TranslateAsync(GalnetSlide.Preview(article.Body), token);
await UpdateSlideAsync(id, generation, s => s with { Teaser = teaser, PreviewTranslated = true, TranslationStatus = "中文 · 自动翻译" });
}
catch (OperationCanceledException) { }
catch (Exception error)
{
App.Runtime.Log("银河新闻翻译", error.Message);
await UpdateSlideAsync(id, generation, s => s with { TranslationStatus = "翻译暂不可用 · 可阅读原文" });
}
finally { previewJobs.Remove((generation, id)); }
}
private async Task UpdateSlideAsync(string id, int generation, Func<GalnetSlide, GalnetSlide> update)
{
if (generation != presentationGeneration || !active) return;
var index = slides.FindIndex(s => s.Id == id); if (index < 0) return;
slides[index] = update(slides[index]); await PushSlidesAsync(); if (articleId == id) await PushArticleAsync();
}
private async Task PushSlidesAsync()
{
if (browserReady) await NewsBrowser.ExecuteScriptAsync("window.galnet?.setSlides(" + JsonSerializer.Serialize(slides, JsonOptions) + ")");
}
private async Task SetCarouselActiveAsync(bool value)
{
try { if (browserReady) await NewsBrowser.ExecuteScriptAsync("window.galnet?.setActive(" + (value ? "true" : "false") + ")"); }
catch (Exception error) { App.Runtime.Log("银河新闻轮播", error.Message); }
}
private async Task PushArticleAsync()
{
if (articleBrowser?.CoreWebView2 is null || slides.FirstOrDefault(s => s.Id == articleId) is not { } slide) return;
await articleBrowser.ExecuteScriptAsync("window.galnet?.setArticle(" + JsonSerializer.Serialize(slide, JsonOptions) + ")");
}
private async Task TranslateBodyAsync(string id, int generation, CancellationToken token)
{
try
{
var slide = slides.First(s => s.Id == id);
var title = await translator.TranslateAsync(slide.Article.Title, token);
var body = await translator.TranslateAsync(slide.Article.Body, token);
await UpdateSlideAsync(id, generation, s => s with { Title = title, Body = body, BodyTranslated = true, TranslationStatus = "中文 · 自动翻译" });
}
catch (OperationCanceledException) { }
catch (Exception error)
{
App.Runtime.Log("银河新闻全文翻译", error.Message);
await UpdateSlideAsync(id, generation, s => s with { TranslationStatus = "翻译暂不可用 · 可阅读原文" });
}
}
private async Task OpenArticleAsync(string id)
{
if (dialogOpen || !active || presentation is null) return;
dialogOpen = true; articleId = id;
await SetCarouselActiveAsync(false);
var readerRoot = XamlRoot;
var readerWidth = Math.Clamp(readerRoot.Size.Width - 100, 320, 1280);
// WebView2 has no useful intrinsic desired width. Give the dialog's
// auto-sized content a real width instead of its 320px minimum layout.
var browser = new WebView2 { DefaultBackgroundColor = Color.FromArgb(255, 3, 3, 5), Width = readerWidth - 48,
Height = Math.Clamp(readerRoot.Size.Height - 200, 260, 900), HorizontalAlignment = HorizontalAlignment.Stretch };
var dialog = new ContentDialog { Title = "GALNET · 银河新闻", CloseButtonText = "关闭", Content = browser, XamlRoot = readerRoot,
Width = readerWidth, MaxWidth = 1280, HorizontalContentAlignment = HorizontalAlignment.Stretch, Background = new SolidColorBrush(Color.FromArgb(255, 3, 3, 5)) };
dialog.Resources["ContentDialogMaxWidth"] = 1280d;
void ResizeReader(XamlRoot root, XamlRootChangedEventArgs args)
{
dialog.Width = Math.Clamp(root.Size.Width - 100, 320, 1280);
browser.Width = dialog.Width - 48; browser.Height = Math.Clamp(root.Size.Height - 200, 260, 900);
}
readerRoot.Changed += ResizeReader;
articleBrowser = browser;
dialog.Opened += async (_, _) =>
{
try
{
await InitializeBrowserAsync(browser, true);
_ = TranslateBodyAsync(id, presentationGeneration, presentation.Token);
}
catch (Exception error) { App.Runtime.Log("银河新闻阅读", error.Message); }
};
try { await dialog.ShowAsync(); }
finally { readerRoot.Changed -= ResizeReader; articleBrowser = null; articleId = null; browser.Close(); dialogOpen = false; await SetCarouselActiveAsync(active); }
}
private void SetBusy(bool busy)
{
RefreshText.Text = busy ? "取消" : "刷新";
RefreshIcon.Visibility = busy ? Visibility.Collapsed : Visibility.Visible;
RefreshProgress.Visibility = busy ? Visibility.Visible : Visibility.Collapsed;
RefreshProgress.IsActive = busy;
Microsoft.UI.Xaml.Automation.AutomationProperties.SetName(RefreshButton, busy ? "取消刷新银河新闻" : "刷新银河新闻");
}
}