Crunchy-Downloader/CRD/ViewModels/UpdateViewModel.cs
Elwador 5b33d2336c Add - Added **retry** for license key requests
Add - Added **Sonarr season/episode numbers** to the history view
Add - Added option to **match episodes to Sonarr episodes** to correct mismatches
Add - Added **button to rematch all Sonarr episodes**
Add - Added an **optional secondary endpoint**
Chg - Changed **changelog heading sizes**
Chg - Changed Sonarr matching to only process **new or unmatched episodes**
Chg - Changed logic to **request audio license keys only when needed**
Fix - Fixed **Sonarr series manual matching dialog**
2025-06-17 18:56:24 +02:00

306 lines
No EOL
10 KiB
C#

using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Text.RegularExpressions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
using Markdig;
using Markdig.Syntax;
using Markdig.Syntax.Inlines;
using Inline = Markdig.Syntax.Inlines.Inline;
namespace CRD.ViewModels;
public partial class UpdateViewModel : ViewModelBase{
[ObservableProperty]
private bool _updateAvailable;
[ObservableProperty]
private bool _updating;
[ObservableProperty]
private double _progress;
[ObservableProperty]
private bool _failed;
private AccountPageViewModel accountPageViewModel;
[ObservableProperty]
private string _currentVersion;
public ObservableCollection<Control> ChangelogBlocks{ get; } = new();
public UpdateViewModel(){
var version = Assembly.GetExecutingAssembly().GetName().Version;
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
LoadChangelog();
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
}
[RelayCommand]
public void StartUpdate(){
Updating = true;
ProgramManager.Instance.NavigationLock = true;
_ = Updater.Instance.DownloadAndUpdateAsync();
}
private void Progress_PropertyChanged(object? sender, PropertyChangedEventArgs e){
if (e.PropertyName == nameof(Updater.Instance.progress)){
Progress = Updater.Instance.progress;
} else if (e.PropertyName == nameof(Updater.Instance.failed)){
Failed = Updater.Instance.failed;
ProgramManager.Instance.NavigationLock = !Failed;
}
}
#region Changelog Builder
private int textSize = 16;
public void LoadChangelog(){
string changelogPath = "CHANGELOG.md";
if (!File.Exists(changelogPath)){
ChangelogBlocks.Clear();
ChangelogBlocks.Add(new TextBlock{
Text = "No changelog found",
FontSize = 16,
TextWrapping = TextWrapping.Wrap
});
return;
}
string markdownText = File.ReadAllText(changelogPath);
markdownText = PreprocessMarkdown(markdownText);
var pipeline = new MarkdownPipelineBuilder().Build();
var document = Markdown.Parse(markdownText, pipeline);
ChangelogBlocks.Clear();
try{
foreach (var block in document){
switch (block){
case HeadingBlock heading:
string headingText = string.Concat(heading.Inline.Select(i => i.ToString()));
ChangelogBlocks.Add(new TextBlock{
Text = headingText,
FontSize = heading.Level switch{ 1 => textSize + 10, 2 => textSize + 6, _ => textSize + 4 },
FontWeight = FontWeight.Bold,
Margin = new Thickness(0, 20, 0, 5),
TextWrapping = TextWrapping.Wrap
});
break;
case ParagraphBlock paragraph:
var inlineControls = BuildInlineControls(paragraph.Inline?.FirstChild);
var container = new WrapPanel{
Margin = new Thickness(0, 5, 0, 5)
};
foreach (var ctrl in inlineControls)
container.Children.Add(ctrl);
ChangelogBlocks.Add(container);
break;
case ListBlock list:
foreach (ListItemBlock item in list){
foreach (var blocki in item){
if (blocki is ParagraphBlock para){
var container1 = new WrapPanel{ Margin = new Thickness(10, 2, 0, 2) };
container1.Children.Add(new TextBlock{ Text = "• ", FontWeight = FontWeight.Bold, FontSize = textSize});
foreach (var ctrl in BuildInlineControls(para.Inline?.FirstChild))
container1.Children.Add(ctrl);
ChangelogBlocks.Add(container1);
}
}
}
break;
}
}
} catch (Exception e){
Console.Error.WriteLine(e);
}
}
IEnumerable<Control> BuildInlineControls(Inline? inline){
var controls = new List<Control>();
var urlRegex = new Regex(@"https?://[^\s]+", RegexOptions.Compiled);
var githubRefRegex = new Regex(
@"https://github\.com/[^/]+/[^/]+/(issues|discussions)/(\d+)",
RegexOptions.Compiled);
while (inline != null){
switch (inline){
case LiteralInline lit:
var text = lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length);
var lastIndex = 0;
foreach (Match match in urlRegex.Matches(text)){
if (match.Index > lastIndex){
controls.Add(new TextBlock{
Text = text.Substring(lastIndex, match.Index - lastIndex),
TextWrapping = TextWrapping.Wrap,
FontSize = textSize
});
}
string url = match.Value;
string buttonText = url;
var ghMatch = githubRefRegex.Match(url);
if (ghMatch.Success && ghMatch.Groups.Count > 2){
buttonText = $"#{ghMatch.Groups[2].Value}";
}
controls.Add(CreateLinkButton(buttonText, url));
lastIndex = match.Index + match.Length;
}
if (lastIndex < text.Length){
controls.Add(new TextBlock{
Text = text.Substring(lastIndex),
TextWrapping = TextWrapping.Wrap,
FontSize = textSize
});
}
break;
case EmphasisInline emph:
var emphControls = BuildInlineControls(emph.FirstChild);
foreach (var ec in emphControls){
if (ec is TextBlock tb){
tb.FontWeight = emph.DelimiterChar == '*' ? FontWeight.Bold : FontWeight.Normal;
tb.FontStyle = emph.DelimiterChar == '_' ? FontStyle.Italic : FontStyle.Normal;
}
controls.Add(ec);
}
break;
case LinkInline link:
var linkText = ConvertInlinesToText(link.FirstChild);
controls.Add(CreateLinkButton(linkText, link.Url));
break;
}
inline = inline.NextSibling;
}
return controls;
}
string ConvertInlinesToText(Inline? inline){
var result = new StringBuilder();
while (inline != null){
switch (inline){
case LiteralInline lit:
result.Append(lit.Content.Text.Substring(lit.Content.Start, lit.Content.Length));
break;
case EmphasisInline emph:
result.Append(ConvertInlinesToText(emph.FirstChild));
break;
case LinkInline link:
var linkText = ConvertInlinesToText(link.FirstChild);
result.Append($"{linkText} ({link.Url})");
break;
case LineBreakInline:
result.Append('\n');
break;
default:
if (inline is ContainerInline{ FirstChild: not null } container){
result.Append(ConvertInlinesToText(container.FirstChild));
}
break;
}
inline = inline.NextSibling;
}
return result.ToString();
}
Brush GetLinkBrush(){
try{
var color = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor ?? Brushes.LightBlue.Color.ToString());
return new SolidColorBrush(color);
} catch{
return new SolidColorBrush(Brushes.LightBlue.Color);
}
}
Button CreateLinkButton(string text, string url){
var button = new Button{
Content = new TextBlock{
Text = text,
FontSize = textSize,
Foreground = GetLinkBrush(),
TextDecorations = TextDecorations.Underline
},
Background = Brushes.Transparent,
BorderThickness = new Thickness(0),
Padding = new Thickness(0),
Cursor = new Cursor(StandardCursorType.Hand)
};
button.Click += (_, __) => {
try{
using var p = new Process();
p.StartInfo = new ProcessStartInfo{
FileName = url,
UseShellExecute = true
};
p.Start();
} catch{
Console.Error.WriteLine($"Failed to open link: {url}");
}
};
return button;
}
private string PreprocessMarkdown(string markdownText){
string detailsPattern = @"<details>\s*<summary>.*?<\/summary>\s*<img\s+src=['""]([^'""]+)['""]\s+alt=['""]([^'""]+)['""]\s*\/?>\s*<\/details>";
return Regex.Replace(markdownText, detailsPattern, match => {
string imageUrl = match.Groups[1].Value;
string altText = match.Groups[2].Value;
return $"![{altText}]({imageUrl})";
});
}
#endregion
}