Add - Added a button to manually match Sonarr series

Add - Available dubs/subs overall to history series - not all dubs/subs are available for every season/episode
Fix - Subscription end date was in UTC, causing the program to not recognize an active premium subscription  https://github.com/Crunchy-DL/Crunchy-Downloader/issues/74
This commit is contained in:
Elwador 2024-08-15 02:27:49 +02:00
parent 29376c59a6
commit 6aa10cb2c2
24 changed files with 843 additions and 508 deletions

View File

@ -10,6 +10,13 @@
<crd:ViewLocator />
</Application.DataTemplates>
<Application.Resources>
<x:Double x:Key="ContentDialogMinWidth">500</x:Double>
<x:Double x:Key="ContentDialogMaxWidth">1500</x:Double>
<x:Double x:Key="ContentDialogMinHeight">150</x:Double>
<x:Double x:Key="ContentDialogMaxHeight">700</x:Double>
</Application.Resources>
<Application.Styles>
<sty:FluentAvaloniaTheme PreferSystemTheme="True" PreferUserAccentColor="True"/>
<StyleInclude Source="avares://CRD/Styling/ControlsGalleryStyles.axaml" />

View File

@ -153,7 +153,6 @@ public class CalendarManager{
}
CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>();
@ -173,7 +172,7 @@ public class CalendarManager{
var firstDayOfWeek = week.CalendarDays.First().DateTime;
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200,firstDayOfWeek, true);
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200, firstDayOfWeek, true);
if (newEpisodesBase is{ Data.Count: > 0 }){
var newEpisodes = newEpisodesBase.Data;
@ -181,10 +180,6 @@ public class CalendarManager{
foreach (var crBrowseEpisode in newEpisodes){
var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic;
if (targetDate.Kind == DateTimeKind.Utc){
targetDate = targetDate.ToLocalTime();
}
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){
continue;
@ -218,6 +213,18 @@ public class CalendarManager{
calendarDay.CalendarEpisodes?.Add(calEpisode);
}
}
foreach (var weekCalendarDay in week.CalendarDays){
if (weekCalendarDay.CalendarEpisodes != null)
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
.OrderBy(e => e.DateTime)
.ThenBy(e => e.SeasonName)
.ThenBy(e => {
double parsedNumber;
return double.TryParse(e.EpisodeNumber, out parsedNumber) ? parsedNumber : double.MinValue;
})
.ToList();
}
}

View File

@ -48,7 +48,7 @@ public class CrAuth{
}
private void JsonTokenToFileAndVariable(string content){
crunInstance.Token = JsonConvert.DeserializeObject<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
crunInstance.Token = Helpers.Deserialize<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
if (crunInstance.Token != null && crunInstance.Token.expires_in != null){

View File

@ -166,7 +166,7 @@ public class CrunchyrollManager{
if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
if (!string.IsNullOrEmpty(decompressedJson)){
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(decompressedJson) ?? new ObservableCollection<HistorySeries>();
HistoryList = Helpers.Deserialize<ObservableCollection<HistorySeries>>(decompressedJson,CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? new ObservableCollection<HistorySeries>();
foreach (var historySeries in HistoryList){
historySeries.Init();
@ -1611,7 +1611,7 @@ public class CrunchyrollManager{
Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>()
};
var playStream = JsonConvert.DeserializeObject<CrunchyStreamData>(responseContent, SettingsJsonSerializerSettings);
var playStream = Helpers.Deserialize<CrunchyStreamData>(responseContent, SettingsJsonSerializerSettings);
if (playStream == null) return temppbData;
if (!string.IsNullOrEmpty(playStream.Token)){
@ -1758,7 +1758,7 @@ public class CrunchyrollManager{
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest);
if (showRequestResponse.IsOk){
CrunchyOldChapter chapterData = JsonConvert.DeserializeObject<CrunchyOldChapter>(showRequestResponse.ResponseContent);
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent,SettingsJsonSerializerSettings);
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);

View File

@ -379,12 +379,22 @@ public class History(){
}
}
private CrSeriesBase? cachedSeries;
private async Task RefreshSeriesData(string seriesId, HistorySeries historySeries){
var series = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (series?.Data != null){
historySeries.SeriesDescription = series.Data.First().Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(series);
historySeries.SeriesTitle = series.Data.First().Title;
if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){
cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
} else{
return;
}
if (cachedSeries?.Data != null){
var series = cachedSeries.Data.First();
historySeries.SeriesDescription = series.Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries);
historySeries.SeriesTitle = series.Title;
historySeries.HistorySeriesAvailableDubLang = series.AudioLocales;
historySeries.HistorySeriesAvailableSoftSubs = series.SubtitleLocales;
}
}
@ -758,13 +768,13 @@ public class History(){
return highestSimilarity < 0.8 ? null : closestMatch;
}
private double CalculateSimilarity(string source, string target){
public double CalculateSimilarity(string source, string target){
int distance = LevenshteinDistance(source, target);
return 1.0 - (double)distance / Math.Max(source.Length, target.Length);
}
public int LevenshteinDistance(string source, string target){
private int LevenshteinDistance(string source, string target){
if (string.IsNullOrEmpty(source)){
return string.IsNullOrEmpty(target) ? 0 : target.Length;
}

View File

@ -7,6 +7,7 @@ using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Parser.Utils;
using CRD.Utils.Structs;
using Newtonsoft.Json;
@ -62,7 +63,7 @@ public class HlsDownloader{
try{
Console.WriteLine("Resume data found! Trying to resume...");
string resumeFileContent = File.ReadAllText($"{fn}.resume");
var resumeData = JsonConvert.DeserializeObject<ResumeData>(resumeFileContent);
var resumeData = Helpers.Deserialize<ResumeData>(resumeFileContent, null);
if (resumeData != null){
if (resumeData.Total == _data.M3U8Json?.Segments.Count &&

View File

@ -9,6 +9,7 @@ using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CRD.Utils.JsonConv;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using Newtonsoft.Json;
@ -16,15 +17,11 @@ using Newtonsoft.Json;
namespace CRD.Utils;
public class Helpers{
/// <summary>
/// Deserializes a JSON string into a specified .NET type.
/// </summary>
/// <typeparam name="T">The type of the object to deserialize to.</typeparam>
/// <param name="json">The JSON string to deserialize.</param>
/// <param name="serializerSettings">The settings for deserialization if null default settings will be used</param>
/// <returns>The deserialized object of type T.</returns>
public static T? Deserialize<T>(string json, JsonSerializerSettings? serializerSettings){
try{
serializerSettings ??= new JsonSerializerSettings();
serializerSettings.Converters.Add(new UtcToLocalTimeConverter());
return JsonConvert.DeserializeObject<T>(json, serializerSettings);
} catch (JsonException ex){
Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}");
@ -55,26 +52,6 @@ public class Helpers{
return dialogue;
}
public static string ExtractDialogue(string[] lines, int startLine){
var dialogueBuilder = new StringBuilder();
for (int i = startLine; i < lines.Length && !string.IsNullOrWhiteSpace(lines[i]); i++){
if (!lines[i].Contains("-->") && !lines[i].StartsWith("STYLE")){
string line = lines[i].Trim();
// Remove HTML tags and keep the inner text
line = Regex.Replace(line, @"<[^>]+>", "");
dialogueBuilder.Append(line + "\\N");
}
}
// Remove the last newline character
if (dialogueBuilder.Length > 0){
dialogueBuilder.Length -= 2; // Remove the last "\N"
}
return dialogueBuilder.ToString();
}
public static void OpenUrl(string url){
try{
Process.Start(new ProcessStartInfo{
@ -423,4 +400,5 @@ public class Helpers{
return languageGroups;
}
}

View File

@ -0,0 +1,19 @@
using System;
using Newtonsoft.Json;
namespace CRD.Utils.JsonConv;
public class UtcToLocalTimeConverter : JsonConverter<DateTime>{
public override DateTime ReadJson(JsonReader reader, Type objectType, DateTime existingValue, bool hasExistingValue, JsonSerializer serializer){
return reader.Value switch{
null => DateTime.MinValue,
DateTime dateTime when dateTime.Kind == DateTimeKind.Utc => dateTime.ToLocalTime(),
DateTime dateTime => dateTime,
_ => throw new JsonSerializationException($"Unexpected token parsing date. Expected DateTime, got {reader.Value.GetType()}.")
};
}
public override void WriteJson(JsonWriter writer, DateTime value, JsonSerializer serializer){
writer.WriteValue(value);
}
}

View File

@ -129,7 +129,10 @@ public class SonarrSeries{
/// The images.
/// </value>
[JsonProperty("images")]
public List<SonarrImage> Images{ get; set; }
public List<SonarrImage>? Images{ get; set; }
[JsonIgnore]
public string ImageUrl{ get; set; }
/// <summary>
/// Gets or sets the type of the series.

View File

@ -109,7 +109,7 @@ public class SonarrClient{
List<SonarrSeries> series = [];
try{
series = JsonConvert.DeserializeObject<List<SonarrSeries>>(json) ?? [];
series = Helpers.Deserialize<List<SonarrSeries>>(json,null) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetSeries error \n" + e);
Console.Error.WriteLine("Sonarr GetSeries error \n" + e);
@ -124,7 +124,7 @@ public class SonarrClient{
List<SonarrEpisode> episodes = [];
try{
episodes = JsonConvert.DeserializeObject<List<SonarrEpisode>>(json) ?? [];
episodes = Helpers.Deserialize<List<SonarrEpisode>>(json,null) ?? [];
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetEpisodes error \n" + e);
Console.Error.WriteLine("Sonarr GetEpisodes error \n" + e);
@ -138,7 +138,7 @@ public class SonarrClient{
var json = await GetJson($"/v3/episode/id={episodeId}");
var episode = new SonarrEpisode();
try{
episode = JsonConvert.DeserializeObject<SonarrEpisode>(json) ?? new SonarrEpisode();
episode = Helpers.Deserialize<SonarrEpisode>(json,null) ?? new SonarrEpisode();
} catch (Exception e){
MainWindow.Instance.ShowError("Sonarr GetEpisode error \n" + e);
Console.Error.WriteLine("Sonarr GetEpisode error \n" + e);

View File

@ -14,7 +14,7 @@ public class StreamError{
public static StreamError? FromJson(string json){
try{
return JsonConvert.DeserializeObject<StreamError>(json);
return Helpers.Deserialize<StreamError>(json,null);
} catch (Exception e){
Console.Error.WriteLine(e);
return null;

View File

@ -52,6 +52,12 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("history_series_add_date")]
public DateTime? HistorySeriesAddDate{ get; set; }
[JsonProperty("history_series_available_soft_subs")]
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_series_available_dub_lang")]
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
[JsonProperty("history_series_soft_subs_override")]
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[];

View File

@ -49,9 +49,9 @@ public class Updater : INotifyPropertyChanged{
public async Task<bool> CheckForUpdatesAsync(){
try{
using (var client = new HttpClient()){
client.DefaultRequestHeaders.Add("User-Agent", "C# App"); // GitHub API requires a user agent
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
var response = await client.GetStringAsync(apiEndpoint);
var releaseInfo = JsonConvert.DeserializeObject<dynamic>(response);
var releaseInfo = Helpers.Deserialize<dynamic>(response,null);
var latestVersion = releaseInfo.tag_name;
downloadUrl = releaseInfo.assets[0].browser_download_url;
@ -63,11 +63,11 @@ public class Updater : INotifyPropertyChanged{
if (latestVersion != currentVersion){
Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion);
return true;
} else{
}
Console.WriteLine("No updates available.");
return false;
}
}
} catch (Exception e){
Console.Error.WriteLine("Failed to get Update information");
return false;

View File

@ -111,7 +111,7 @@ public partial class AccountPageViewModel : ViewModelBase{
CloseButtonText = "Close"
};
var viewModel = new ContentDialogInputLoginViewModel(dialog, this);
var viewModel = new Utils.ContentDialogInputLoginViewModel(dialog, this);
dialog.Content = new ContentDialogInputLoginView(){
DataContext = viewModel
};

View File

@ -13,7 +13,10 @@ using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils;
using CRD.Views;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
namespace CRD.ViewModels;
@ -28,24 +31,38 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
public static bool _sonarrAvailable;
[ObservableProperty]
public static bool _sonarrConnected;
private IStorageProvider? _storageProvider;
[ObservableProperty]
private string _availableDubs;
[ObservableProperty]
private string _availableSubs;
public SeriesPageViewModel(){
_selectedSeries = CrunchyrollManager.Instance.SelectedSeries;
if (_selectedSeries.ThumbnailImage == null){
_selectedSeries.LoadImage();
}
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrConnected = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
} else{
SonarrAvailable = false;
}
} else{
SonarrConnected = SonarrAvailable = false;
}
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
}
[RelayCommand]
@ -79,6 +96,44 @@ public partial class SeriesPageViewModel : ViewModelBase{
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
[RelayCommand]
public async Task MatchSonarrSeries_Button(){
var dialog = new ContentDialog(){
Title = "Sonarr Matching",
PrimaryButtonText = "Save",
CloseButtonText = "Close",
FullSizeDesired = true
};
var viewModel = new ContentDialogSonarrMatchViewModel(dialog, SelectedSeries.SonarrSeriesId,SelectedSeries.SeriesTitle);
dialog.Content = new ContentDialogSonarrMatchView(){
DataContext = viewModel
};
var dialogResult = await dialog.ShowAsync();
if (dialogResult == ContentDialogResult.Primary){
SelectedSeries.SonarrSeriesId = viewModel.CurrentSonarrSeries.Id.ToString();
SelectedSeries.SonarrTvDbId = viewModel.CurrentSonarrSeries.TvdbId.ToString();
SelectedSeries.SonarrSlugTitle = viewModel.CurrentSonarrSeries.TitleSlug;
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrConnected = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
} else{
SonarrAvailable = false;
}
} else{
SonarrConnected = SonarrAvailable = false;
}
UpdateData("");
}
}
[RelayCommand]
public async Task DownloadSeasonAll(HistorySeason season){
@ -112,6 +167,9 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.Seasons.Refresh();
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
// MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
}
@ -122,6 +180,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.Seasons.Remove(objectToRemove);
CfgManager.UpdateHistoryFile();
}
MessageBus.Current.SendMessage(new NavigationMessage(typeof(SeriesPageViewModel), false, true));
}

View File

@ -1,12 +1,10 @@
using System;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogInputLoginViewModel : ViewModelBase{
private readonly ContentDialog dialog;

View File

@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models;
using DynamicData;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogSonarrMatchViewModel : ViewModelBase{
private readonly ContentDialog dialog;
[ObservableProperty]
private SonarrSeries _currentSonarrSeries;
[ObservableProperty]
private Bitmap? _currentSeriesImage;
[ObservableProperty]
private SonarrSeries _selectedItem;
[ObservableProperty]
private ObservableCollection<SonarrSeries> _sonarrSeriesList = new();
public ContentDialogSonarrMatchViewModel(ContentDialog dialog, string? currentSonarrId, string? seriesTitle){
if (dialog is null){
throw new ArgumentNullException(nameof(dialog));
}
this.dialog = dialog;
dialog.Closed += DialogOnClosed;
dialog.PrimaryButtonClick += SaveButton;
CurrentSonarrSeries = SonarrClient.Instance.SonarrSeries.Find(e => e.Id.ToString() == currentSonarrId) ?? new SonarrSeries(){ Title = "No series matched" };
SetImageUrl(CurrentSonarrSeries);
LoadList(seriesTitle);
}
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
dialog.PrimaryButtonClick -= SaveButton;
}
private void LoadList(string? title){
var list = PopulateSeriesList(title);
SonarrSeriesList.AddRange(list);
}
private List<SonarrSeries> PopulateSeriesList(string? title){
var seriesList = SonarrClient.Instance.SonarrSeries.ToList();
if (!string.IsNullOrEmpty(title)){
seriesList.Sort((series1, series2) => {
double similarity1 = Helpers.CalculateCosineSimilarity(series1.Title.ToLower(), title.ToLower());
double similarity2 = Helpers.CalculateCosineSimilarity(series2.Title.ToLower(), title.ToLower());
return similarity2.CompareTo(similarity1);
});
} else{
seriesList.Sort((series1, series2) => string.Compare(series1.Title, series2.Title, StringComparison.OrdinalIgnoreCase));
}
seriesList = seriesList.Take(20).ToList();
foreach (var sonarrSeries in seriesList){
SetImageUrl(sonarrSeries);
}
return seriesList;
}
private void SetImageUrl(SonarrSeries sonarrSeries){
var properties = CrunchyrollManager.Instance.CrunOptions.SonarrProperties;
if (properties == null || sonarrSeries.Images == null){
return;
}
var baseUrl = "";
baseUrl = $"http{(properties.UseSsl ? "s" : "")}://{(!string.IsNullOrEmpty(properties.Host) ? properties.Host : "localhost")}:{properties.Port}{(properties.UrlBase ?? "")}";
sonarrSeries.ImageUrl = baseUrl + sonarrSeries.Images.Find(e => e.CoverType == SonarrCoverType.Poster)?.Url;
}
partial void OnSelectedItemChanged(SonarrSeries value){
CurrentSonarrSeries = value;
}
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
dialog.Closed -= DialogOnClosed;
}
}

View File

@ -1,12 +1,10 @@
using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader;
using CRD.Utils.Structs;
using CRD.Utils.Updater;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogUpdateViewModel : ViewModelBase{
private readonly ContentDialog dialog;

View File

@ -9,6 +9,7 @@ using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Windowing;
using ReactiveUI;
using ContentDialogUpdateViewModel = CRD.ViewModels.Utils.ContentDialogUpdateViewModel;
namespace CRD.Views;

View File

@ -27,23 +27,36 @@
<Button Grid.Row="0" Grid.Column="0" Command="{Binding NavBack}" Margin="0 0 0 10">Back</Button>
<Image Grid.Row="1" Grid.Column="0" Margin="10" Source="{Binding SelectedSeries.ThumbnailImage}" Width="240"
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<StackPanel Orientation="Vertical">
<Grid Margin="10" VerticalAlignment="Top" >
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="10" Source="{Binding SelectedSeries.ThumbnailImage}" Width="240"
Height="360">
</Image>
<Grid Grid.Row="1" Grid.Column="1">
<Grid Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="50" Text="{Binding SelectedSeries.SeriesTitle}"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
<StackPanel Grid.Row="3" Orientation="Vertical">
<TextBlock Grid.Row="3" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
<TextBlock Grid.Row="4" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
<StackPanel Grid.Row="5" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
@ -190,6 +203,18 @@
</Popup>
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding MatchSonarrSeries_Button}">
<Grid>
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
</Grid>
</Button>
</StackPanel>
</StackPanel>
@ -197,9 +222,9 @@
</StackPanel>
</Grid>
</Grid>
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2">
<ItemsControl ItemsSource="{Binding SelectedSeries.Seasons}">
<ItemsControl.ItemTemplate>
<DataTemplate>
@ -485,6 +510,11 @@
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels"
x:DataType="vm:ContentDialogInputLoginViewModel"
xmlns:utils="clr-namespace:CRD.ViewModels.Utils"
x:DataType="utils:ContentDialogInputLoginViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.Utils.ContentDialogInputLoginView">

View File

@ -0,0 +1,104 @@
<controls:ContentDialog xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CRD.ViewModels.Utils"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:models="clr-namespace:CRD.Utils.Sonarr.Models"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
x:DataType="vm:ContentDialogSonarrMatchViewModel"
x:Class="CRD.Views.Utils.ContentDialogSonarrMatchView">
<Grid HorizontalAlignment="Stretch" MaxHeight="500" >
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Border Grid.Column="0" Grid.Row="1" CornerRadius="10" Background="{DynamicResource ButtonBackground}">
<Grid Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Image -->
<Image Grid.Column="0" Margin="10" asyncImageLoader:ImageLoader.Source="{Binding CurrentSonarrSeries.ImageUrl}" MaxWidth="120" MaxHeight="180"></Image>
<Grid Grid.Column="1" Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
<ColumnDefinition Width="Auto" />
<!-- Takes up space as needed for the time -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" Grid.Column="0" Text="{Binding CurrentSonarrSeries.Title}" FontWeight="Bold"
FontSize="16"
TextWrapping="Wrap" VerticalAlignment="Center" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding CurrentSonarrSeries.Year}" FontStyle="Italic"
HorizontalAlignment="Right" VerticalAlignment="Center" />
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
Text="{Binding CurrentSonarrSeries.Overview}"
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
<TextBlock Grid.Row="0" Grid.Column="0" FontSize="1" Text=" " Width="1500" Opacity="0" />
</Grid>
</Grid>
</Border>
<!-- <Rectangle Grid.Row="2" Width="1500" Height="0" Fill="Gray" Margin="10,0" /> -->
<!-- <TextBlock Grid.Column="0" Grid.Row="2" Text="Series"></TextBlock> -->
<ListBox Grid.Row="3" SelectedItem="{Binding SelectedItem}" ItemsSource="{Binding SonarrSeriesList}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:SonarrSeries}">
<StackPanel>
<Border Padding="10" Margin="5" BorderThickness="1">
<Grid Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Image -->
<asyncImageLoader:AdvancedImage Grid.Column="0" MaxWidth="120" MaxHeight="180" Source="{Binding ImageUrl}"
Stretch="Fill" />
<!-- Text Content -->
<Grid Grid.Column="1" Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
<ColumnDefinition Width="Auto" />
<!-- Takes up space as needed for the time -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Grid.Column="0" Text="{Binding Title}" FontWeight="Bold"
FontSize="16"
TextWrapping="Wrap" />
<TextBlock Grid.Row="0" Grid.Column="1" Text="{Binding Year}" FontStyle="Italic"
HorizontalAlignment="Right" TextWrapping="Wrap" />
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
Text="{Binding Overview}"
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
</Grid>
</Grid>
</Border>
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</controls:ContentDialog>

View File

@ -0,0 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views.Utils;
public partial class ContentDialogSonarrMatchView : UserControl{
public ContentDialogSonarrMatchView(){
InitializeComponent();
}
}

View File

@ -3,7 +3,8 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels"
x:DataType="vm:ContentDialogUpdateViewModel"
xmlns:utils="clr-namespace:CRD.ViewModels.Utils"
x:DataType="utils:ContentDialogUpdateViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.Utils.ContentDialogUpdateView">