Add - Added Table view to history tab

Add - Added Sorting to history tab
Add - Added next air date to history posters
Add - Added scale slider to history tab for posters
Chg - Auto download always starts new downloads even when you are not on the download queue tab
Chg - Small size adjustment for the text in the calendar tab
Fix - Simultaneous downloads set to 1 and auto download didn't work together
Fix - Finished downloads didn't resume correctly
This commit is contained in:
Elwador 2024-06-27 00:04:50 +02:00
parent 89c7b1021f
commit 9d66eb34c9
21 changed files with 1288 additions and 407 deletions

View File

@ -356,7 +356,7 @@ public class CrSeries(){
return ret; return ret;
} }
public async Task<CrSeriesSearch?> ParseSeriesById(string id,string? locale){ public async Task<CrSeriesSearch?> ParseSeriesById(string id,string? locale,bool forced = false){
if (crunInstance.CmsToken?.Cms == null){ if (crunInstance.CmsToken?.Cms == null){
Console.Error.WriteLine("Missing CMS Access Token"); Console.Error.WriteLine("Missing CMS Access Token");
return null; return null;
@ -366,7 +366,11 @@ public class CrSeries(){
query["preferred_audio_language"] = "ja-JP"; query["preferred_audio_language"] = "ja-JP";
if (!string.IsNullOrEmpty(locale)){ if (!string.IsNullOrEmpty(locale)){
query["locale"] = Languages.Locale2language(locale).CrLocale; query["locale"] = Languages.Locale2language(locale).CrLocale;
if (forced){
query["force_locale"] = Languages.Locale2language(locale).CrLocale;
}
} }

View File

@ -22,6 +22,7 @@ using CRD.Utils.Muxing;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models; using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.ViewModels; using CRD.ViewModels;
using CRD.Views; using CRD.Views;
using HtmlAgilityPack; using HtmlAgilityPack;
@ -104,8 +105,9 @@ public class Crunchyroll{
public Crunchyroll(){ public Crunchyroll(){
CrunOptions = new CrDownloadOptions(); CrunOptions = new CrDownloadOptions();
Queue.CollectionChanged += UpdateItemListOnRemove;
} }
public async Task Init(){ public async Task Init(){
_widevine = Widevine.Instance; _widevine = Widevine.Instance;
@ -199,6 +201,41 @@ public class Crunchyroll{
} }
} }
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
if (e.Action == NotifyCollectionChangedAction.Remove){
if (e.OldItems != null)
foreach (var eOldItem in e.OldItems){
var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem));
if (downloadItem != null){
DownloadItemModels.Remove(downloadItem);
} else{
Console.Error.WriteLine("Failed to Remove Episode from list");
}
}
}
UpdateDownloadListItems();
}
public void UpdateDownloadListItems(){
var list = Queue;
foreach (CrunchyEpMeta crunchyEpMeta in list){
var downloadItem = DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(crunchyEpMeta));
if (downloadItem != null){
downloadItem.Refresh();
} else{
downloadItem = new DownloadItemModel(crunchyEpMeta);
downloadItem.LoadImage();
DownloadItemModels.Add(downloadItem);
}
if (downloadItem is{ isDownloading: false, Error: false } && CrunOptions.AutoDownload && ActiveDownloads < CrunOptions.SimultaneousDownloads){
downloadItem.StartDownload();
}
}
}
public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){ public async Task<CalendarWeek> GetCalendarForDate(string weeksMondayDate, bool forceUpdate){
if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){ if (!forceUpdate && calendar.TryGetValue(weeksMondayDate, out var forDate)){
@ -447,13 +484,12 @@ public class Crunchyroll{
if (CrunOptions.RemoveFinishedDownload){ if (CrunOptions.RemoveFinishedDownload){
Queue.Remove(data); Queue.Remove(data);
} }
Queue.Refresh();
} else{ } else{
Console.WriteLine("Skipping mux"); Console.WriteLine("Skipping mux");
} }
ActiveDownloads--; ActiveDownloads--;
Queue.Refresh();
if (CrunOptions.History && data.Data != null && data.Data.Count > 0){ if (CrunOptions.History && data.Data != null && data.Data.Count > 0){
CrHistory.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId); CrHistory.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId);
@ -506,7 +542,7 @@ public class Crunchyroll{
Console.Error.WriteLine("No xml description file found to mux description"); Console.Error.WriteLine("No xml description file found to mux description");
} }
} }
var merger = new Merger(new MergerOptions{ var merger = new Merger(new MergerOptions{
OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), OnlyVid = data.Where(a => a.Type == DownloadMediaType.Video).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
@ -528,7 +564,7 @@ public class Crunchyroll{
}, },
CcTag = options.CcTag, CcTag = options.CcTag,
mp3 = muxToMp3, mp3 = muxToMp3,
MuxDescription = muxDesc Description = data.Where(a => a.Type == DownloadMediaType.Description).Select(a => new MergerInput{ Path = a.Path ?? string.Empty }).ToList(),
}); });
if (!File.Exists(CfgManager.PathFFMPEG)){ if (!File.Exists(CfgManager.PathFFMPEG)){
@ -1380,6 +1416,11 @@ public class Crunchyroll{
writer.WriteEndElement(); // End Tags writer.WriteEndElement(); // End Tags
writer.WriteEndDocument(); writer.WriteEndDocument();
} }
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description,
Path = fullPath,
});
} }
Console.WriteLine($"{fileName} has been created with the description."); Console.WriteLine($"{fileName} has been created with the description.");

View File

@ -1,16 +1,21 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Sonarr.Models; using CRD.Utils.Sonarr.Models;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
using DynamicData;
using Newtonsoft.Json; using Newtonsoft.Json;
using ReactiveUI; using ReactiveUI;
@ -22,7 +27,7 @@ public class History(){
public async Task UpdateSeries(string seriesId, string? seasonId){ public async Task UpdateSeries(string seriesId, string? seasonId){
await crunInstance.CrAuth.RefreshToken(true); await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja"); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja", true);
if (parsedSeries == null){ if (parsedSeries == null){
Console.Error.WriteLine("Parse Data Invalid"); Console.Error.WriteLine("Parse Data Invalid");
@ -64,7 +69,7 @@ public class History(){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeason != null){ if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
@ -84,7 +89,7 @@ public class History(){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (historySeason != null){ if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
@ -102,9 +107,9 @@ public class History(){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
var downloadDirPath = ""; var downloadDirPath = "";
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){ if (!string.IsNullOrEmpty(historySeries.SeriesDownloadPath)){
downloadDirPath = historySeries.SeriesDownloadPath; downloadDirPath = historySeries.SeriesDownloadPath;
} }
@ -145,7 +150,7 @@ public class History(){
var seriesId = episode.SeriesId; var seriesId = episode.SeriesId;
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == episode.SeasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == episode.SeasonId);
var series = await crunInstance.CrSeries.SeriesById(seriesId); var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){ if (series?.Data != null){
@ -174,7 +179,7 @@ public class History(){
historySeries.Seasons.Add(newSeason); historySeries.Seasons.Add(newSeason);
historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); SortSeasons(historySeries);
} }
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
@ -198,11 +203,7 @@ public class History(){
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} }
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); SortItems();
crunInstance.HistoryList.Clear();
foreach (var item in sortedList){
crunInstance.HistoryList.Add(item);
}
MatchHistorySeriesWithSonarr(false); MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries); await MatchHistoryEpisodesWithSonarr(false, historySeries);
@ -215,7 +216,7 @@ public class History(){
var seriesId = firstEpisode.SeriesId; var seriesId = firstEpisode.SeriesId;
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.Find(s => s.SeasonId == firstEpisode.SeasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == firstEpisode.SeasonId);
var series = await crunInstance.CrSeries.SeriesById(seriesId); var series = await crunInstance.CrSeries.SeriesById(seriesId);
if (series?.Data != null){ if (series?.Data != null){
historySeries.SeriesTitle = series.Data.First().Title; historySeries.SeriesTitle = series.Data.First().Title;
@ -257,7 +258,7 @@ public class History(){
historySeries.Seasons.Add(newSeason); historySeries.Seasons.Add(newSeason);
historySeries.Seasons = historySeries.Seasons.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0).ToList(); SortSeasons(historySeries);
} }
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
@ -286,11 +287,7 @@ public class History(){
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} }
var sortedList = crunInstance.HistoryList.OrderBy(item => item.SeriesTitle).ToList(); SortItems();
crunInstance.HistoryList.Clear();
foreach (var item in sortedList){
crunInstance.HistoryList.Add(item);
}
MatchHistorySeriesWithSonarr(false); MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries); await MatchHistoryEpisodesWithSonarr(false, historySeries);
@ -298,6 +295,66 @@ public class History(){
} }
} }
private void SortSeasons(HistorySeries series){
var sortedSeasons = series.Seasons
.OrderBy(s => s.SeasonNum != null ? int.Parse(s.SeasonNum) : 0)
.ToList();
series.Seasons.Clear();
foreach (var season in sortedSeasons){
series.Seasons.Add(season);
}
}
public void SortItems(){
var currentSortingType = Crunchyroll.Instance.CrunOptions.HistoryPageProperties?.SelectedSorting ?? SortingType.SeriesTitle;
switch (currentSortingType){
case SortingType.SeriesTitle:
var sortedList = Crunchyroll.Instance.HistoryList.OrderBy(s => s.SeriesTitle).ToList();
Crunchyroll.Instance.HistoryList.Clear();
Crunchyroll.Instance.HistoryList.AddRange(sortedList);
return;
case SortingType.NextAirDate:
DateTime today = DateTime.UtcNow.Date;
var sortedSeriesDates = Crunchyroll.Instance.HistoryList
.OrderByDescending(s => s.SonarrNextAirDate == "Today")
.ThenBy(s => s.SonarrNextAirDate == "Today" ? s.SeriesTitle : null)
.ThenBy(s => {
var date = ParseDate(s.SonarrNextAirDate, today);
return date.HasValue ? date.Value : DateTime.MaxValue;
})
.ThenBy(s => s.SeriesTitle)
.ToList();
Crunchyroll.Instance.HistoryList.Clear();
Crunchyroll.Instance.HistoryList.AddRange(sortedSeriesDates);
return;
}
}
public static DateTime? ParseDate(string dateStr, DateTime today){
if (dateStr == "Today"){
return today;
}
if (DateTime.TryParseExact(dateStr, "dd.MM.yyyy", CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime date)){
return date;
}
return null;
}
private string GetSeriesThumbnail(CrSeriesBase series){ private string GetSeriesThumbnail(CrSeriesBase series){
// var series = await crunInstance.CrSeries.SeriesById(seriesId); // var series = await crunInstance.CrSeries.SeriesById(seriesId);
@ -389,6 +446,8 @@ public class History(){
if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){ if (!string.IsNullOrEmpty(historySeries.SonarrSeriesId)){
var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId)); var episodes = await SonarrClient.Instance.GetEpisodes(int.Parse(historySeries.SonarrSeriesId));
historySeries.SonarrNextAirDate = GetNextAirDate(episodes);
List<HistoryEpisode> allHistoryEpisodes =[]; List<HistoryEpisode> allHistoryEpisodes =[];
foreach (var historySeriesSeason in historySeries.Seasons){ foreach (var historySeriesSeason in historySeries.Seasons){
@ -451,6 +510,29 @@ public class History(){
} }
} }
private string GetNextAirDate(List<SonarrEpisode> episodes){
DateTime today = DateTime.UtcNow.Date;
// Check if any episode air date matches today
var todayEpisode = episodes.FirstOrDefault(e => e.AirDateUtc.Date == today);
if (todayEpisode != null){
return "Today";
}
// Find the next episode date
var nextEpisode = episodes
.Where(e => e.AirDateUtc.Date > today)
.OrderBy(e => e.AirDateUtc.Date)
.FirstOrDefault();
if (nextEpisode != null){
return nextEpisode.AirDateUtc.ToString("dd.MM.yyyy");
}
// If no future episode date is found
return string.Empty;
}
private SonarrSeries? FindClosestMatch(string title){ private SonarrSeries? FindClosestMatch(string title){
SonarrSeries? closestMatch = null; SonarrSeries? closestMatch = null;
double highestSimilarity = 0.0; double highestSimilarity = 0.0;
@ -543,218 +625,3 @@ public class NumericStringPropertyComparer : IComparer<HistoryEpisode>{
} }
} }
public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_title")]
public string? SeriesTitle{ get; set; }
[JsonProperty("series_id")]
public string? SeriesId{ get; set; }
[JsonProperty("sonarr_series_id")]
public string? SonarrSeriesId{ get; set; }
[JsonProperty("sonarr_tvdb_id")]
public string? SonarrTvDbId{ get; set; }
[JsonProperty("sonarr_slug_title")]
public string? SonarrSlugTitle{ get; set; }
[JsonProperty("series_description")]
public string? SeriesDescription{ get; set; }
[JsonProperty("series_thumbnail_url")]
public string? ThumbnailImageUrl{ get; set; }
[JsonProperty("series_new_episodes")]
public int NewEpisodes{ get; set; }
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonProperty("series_season_list")]
public required List<HistorySeason> Seasons{ get; set; }
[JsonProperty("series_download_path")]
public string? SeriesDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
public bool FetchingData{ get; set; }
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ThumbnailImage = new Bitmap(stream);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
}
}
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
public void UpdateNewEpisodes(){
int count = 0;
bool foundWatched = false;
// Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){
continue;
}
// Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue;
}
if (!Seasons[i].EpisodesList[j].WasDownloaded){
count++;
} else{
foundWatched = true;
}
}
}
NewEpisodes = count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
}
public void SetFetchingData(){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
}
public async Task AddNewMissingToDownloads(){
bool foundWatched = false;
// Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){
continue;
}
// Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue;
}
if (!Seasons[i].EpisodesList[j].WasDownloaded){
//ADD to download queue
await Seasons[i].EpisodesList[j].DownloadEpisode();
} else{
foundWatched = true;
}
}
}
}
public async Task FetchData(string? seasonId){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this);
}
}
public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("season_title")]
public string? SeasonTitle{ get; set; }
[JsonProperty("season_id")]
public string? SeasonId{ get; set; }
[JsonProperty("season_cr_season_number")]
public string? SeasonNum{ get; set; }
[JsonProperty("season_special_season")]
public bool? SpecialSeason{ get; set; }
[JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
[JsonProperty("season_downloaded_episodes")]
public int DownloadedEpisodes{ get; set; }
[JsonProperty("season_episode_list")]
public required List<HistoryEpisode> EpisodesList{ get; set; }
[JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public void UpdateDownloaded(string? EpisodeId){
if (!string.IsNullOrEmpty(EpisodeId)){
EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded();
}
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
public void UpdateDownloaded(){
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
public partial class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_title")]
public string? EpisodeTitle{ get; set; }
[JsonProperty("episode_id")]
public string? EpisodeId{ get; set; }
[JsonProperty("episode_cr_episode_number")]
public string? Episode{ get; set; }
[JsonProperty("episode_cr_episode_description")]
public string? EpisodeDescription{ get; set; }
[JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_was_downloaded")]
public bool WasDownloaded{ get; set; }
[JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; }
[JsonProperty("sonarr_episode_id")]
public string? SonarrEpisodeId{ get; set; }
[JsonProperty("sonarr_has_file")]
public bool SonarrHasFile{ get; set; }
[JsonProperty("sonarr_episode_number")]
public string? SonarrEpisodeNumber{ get; set; }
[JsonProperty("sonarr_season_number")]
public string? SonarrSeasonNumber{ get; set; }
[JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){
WasDownloaded = !WasDownloaded;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
}
public async Task DownloadEpisode(){
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
}
}

View File

@ -163,6 +163,9 @@ public enum DownloadMediaType{
[EnumMember(Value = "Subtitle")] [EnumMember(Value = "Subtitle")]
Subtitle, Subtitle,
[EnumMember(Value = "Description")]
Description,
} }
public enum ScaledBorderAndShadowSelection{ public enum ScaledBorderAndShadowSelection{
@ -171,6 +174,18 @@ public enum ScaledBorderAndShadowSelection{
ScaledBorderAndShadowNo, ScaledBorderAndShadowNo,
} }
public enum HistoryViewType{
Posters,
Table,
}
public enum SortingType{
[EnumMember(Value = "Series Title")]
SeriesTitle,
[EnumMember(Value = "Next Air Date")]
NextAirDate,
}
public enum SonarrCoverType{ public enum SonarrCoverType{
Banner, Banner,
FanArt, FanArt,

View File

@ -71,6 +71,15 @@ public class HlsDownloader{
_data.Offset = resumeData.Completed; _data.Offset = resumeData.Completed;
_data.IsResume = true; _data.IsResume = true;
} else{ } else{
if (resumeData.Total == _data.M3U8Json?.Segments.Count &&
resumeData.Completed == resumeData.Total &&
!double.IsNaN(resumeData.Completed)){
Console.WriteLine("Already finished");
return (Ok: true, _data.Parts);
}
Console.WriteLine("Resume data is wrong!"); Console.WriteLine("Resume data is wrong!");
Console.WriteLine($"Resume: {{ total: {resumeData.Total}, dled: {resumeData.Completed} }}, " + Console.WriteLine($"Resume: {{ total: {resumeData.Total}, dled: {resumeData.Completed} }}, " +
$"Current: {{ total: {_data.M3U8Json?.Segments.Count} }}"); $"Current: {{ total: {_data.M3U8Json?.Segments.Count} }}");

View File

@ -27,6 +27,17 @@ public class Helpers{
} }
} }
public static void OpenUrl(string url){
try{
Process.Start(new ProcessStartInfo{
FileName = url,
UseShellExecute = true
});
} catch (Exception e){
Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}");
}
}
public static void EnsureDirectoriesExist(string path){ public static void EnsureDirectoriesExist(string path){
// Check if the path is absolute // Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path); bool isAbsolute = Path.IsPathRooted(path);

View File

@ -141,10 +141,12 @@ public class Merger{
foreach (var aud in options.OnlyAudio){ foreach (var aud in options.OnlyAudio){
string trackName = aud.Language.Name; string trackName = aud.Language.Name;
args.Add("--audio-tracks 0");
args.Add("--no-video");
args.Add($"--track-name 0:\"{trackName}\""); args.Add($"--track-name 0:\"{trackName}\"");
args.Add($"--language 0:{aud.Language.Code}"); args.Add($"--language 0:{aud.Language.Code}");
args.Add("--no-video");
args.Add("--audio-tracks 0");
if (options.Defaults.Audio.Code == aud.Language.Code){ if (options.Defaults.Audio.Code == aud.Language.Code){
args.Add("--default-track 0"); args.Add("--default-track 0");
@ -181,7 +183,7 @@ public class Merger{
args.Add("--no-subtitles"); args.Add("--no-subtitles");
} }
if (options.Fonts != null && options.Fonts.Count > 0){ if (options.Fonts is{ Count: > 0 }){
foreach (var font in options.Fonts){ foreach (var font in options.Fonts){
args.Add($"--attachment-name \"{font.Name}\""); args.Add($"--attachment-name \"{font.Name}\"");
args.Add($"--attachment-mime-type \"{font.Mime}\""); args.Add($"--attachment-mime-type \"{font.Mime}\"");
@ -191,7 +193,7 @@ public class Merger{
args.Add("--no-attachments"); args.Add("--no-attachments");
} }
if (options.Chapters != null && options.Chapters.Count > 0){ if (options.Chapters is{ Count: > 0 }){
args.Add($"--chapters \"{options.Chapters[0].Path}\""); args.Add($"--chapters \"{options.Chapters[0].Path}\"");
} }
@ -199,8 +201,8 @@ public class Merger{
args.Add($"--title \"{options.VideoTitle}\""); args.Add($"--title \"{options.VideoTitle}\"");
} }
if (options.MuxDescription){ if (options.Description is{ Count: > 0 }){
args.Add($"--global-tags \"{Path.Combine(Path.GetDirectoryName(options.Output), Path.GetFileNameWithoutExtension(options.Output))}.xml\""); args.Add($"--global-tags \"{options.Description[0].Path}\"");
} }
@ -242,10 +244,8 @@ public class Merger{
allMediaFiles.ForEach(file => DeleteFile(file.Path)); allMediaFiles.ForEach(file => DeleteFile(file.Path));
allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume")); allMediaFiles.ForEach(file => DeleteFile(file.Path + ".resume"));
if (options.MuxDescription){ options.Description?.ForEach(chapter => DeleteFile(chapter.Path));
DeleteFile(Path.Combine(Path.GetDirectoryName(options.Output), Path.GetFileNameWithoutExtension(options.Output)) + ".xml");
}
// Delete chapter files if any // Delete chapter files if any
options.Chapters?.ForEach(chapter => DeleteFile(chapter.Path)); options.Chapters?.ForEach(chapter => DeleteFile(chapter.Path));
@ -319,7 +319,7 @@ public class MergerOptions{
public MuxOptions Options{ get; set; } public MuxOptions Options{ get; set; }
public Defaults Defaults{ get; set; } public Defaults Defaults{ get; set; }
public bool mp3{ get; set; } public bool mp3{ get; set; }
public bool MuxDescription{ get; set; } public List<MergerInput> Description{ get; set; } = new List<MergerInput>();
} }
public class MuxOptions{ public class MuxOptions{

View File

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.ViewModels;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
@ -143,4 +144,7 @@ public class CrDownloadOptions{
[YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)] [YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)]
public string? DownloadDirPath{ get; set; } public string? DownloadDirPath{ get; set; }
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
} }

View File

@ -0,0 +1,55 @@
using System.ComponentModel;
using System.Threading.Tasks;
using CRD.Downloader;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_title")]
public string? EpisodeTitle{ get; set; }
[JsonProperty("episode_id")]
public string? EpisodeId{ get; set; }
[JsonProperty("episode_cr_episode_number")]
public string? Episode{ get; set; }
[JsonProperty("episode_cr_episode_description")]
public string? EpisodeDescription{ get; set; }
[JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_was_downloaded")]
public bool WasDownloaded{ get; set; }
[JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; }
[JsonProperty("sonarr_episode_id")]
public string? SonarrEpisodeId{ get; set; }
[JsonProperty("sonarr_has_file")]
public bool SonarrHasFile{ get; set; }
[JsonProperty("sonarr_episode_number")]
public string? SonarrEpisodeNumber{ get; set; }
[JsonProperty("sonarr_season_number")]
public string? SonarrSeasonNumber{ get; set; }
[JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){
WasDownloaded = !WasDownloaded;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(WasDownloaded)));
}
public async Task DownloadEpisode(){
await Crunchyroll.Instance.AddEpisodeToQue(EpisodeId, Crunchyroll.Instance.DefaultLocale, Crunchyroll.Instance.CrunOptions.DubLang);
}
}

View File

@ -0,0 +1,51 @@
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using CRD.Downloader;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("season_title")]
public string? SeasonTitle{ get; set; }
[JsonProperty("season_id")]
public string? SeasonId{ get; set; }
[JsonProperty("season_cr_season_number")]
public string? SeasonNum{ get; set; }
[JsonProperty("season_special_season")]
public bool? SpecialSeason{ get; set; }
[JsonIgnore]
public string CombinedProperty => SpecialSeason ?? false ? $"Specials {SeasonNum}" : $"Season {SeasonNum}";
[JsonProperty("season_downloaded_episodes")]
public int DownloadedEpisodes{ get; set; }
[JsonProperty("season_episode_list")]
public required List<HistoryEpisode> EpisodesList{ get; set; }
[JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public void UpdateDownloaded(string? EpisodeId){
if (!string.IsNullOrEmpty(EpisodeId)){
EpisodesList.First(e => e.EpisodeId == EpisodeId).ToggleWasDownloaded();
}
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
public void UpdateDownloaded(){
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}

View File

@ -0,0 +1,175 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CRD.Downloader;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("series_title")]
public string? SeriesTitle{ get; set; }
[JsonProperty("series_id")]
public string? SeriesId{ get; set; }
[JsonProperty("sonarr_series_id")]
public string? SonarrSeriesId{ get; set; }
[JsonProperty("sonarr_tvdb_id")]
public string? SonarrTvDbId{ get; set; }
[JsonProperty("sonarr_slug_title")]
public string? SonarrSlugTitle{ get; set; }
[JsonProperty("sonarr_next_air_date")]
public string? SonarrNextAirDate{ get; set; }
[JsonProperty("series_description")]
public string? SeriesDescription{ get; set; }
[JsonProperty("series_thumbnail_url")]
public string? ThumbnailImageUrl{ get; set; }
[JsonProperty("series_new_episodes")]
public int NewEpisodes{ get; set; }
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonProperty("series_season_list")]
public required ObservableCollection<HistorySeason> Seasons{ get; set; }
[JsonProperty("series_download_path")]
public string? SeriesDownloadPath{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
[JsonIgnore]
public bool FetchingData{ get; set; }
[JsonIgnore]
public bool EditModeEnabled{
get => _editModeEnabled;
set{
if (_editModeEnabled != value){
_editModeEnabled = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(EditModeEnabled)));
}
}
}
[JsonIgnore]
private bool _editModeEnabled;
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ThumbnailImage = new Bitmap(stream);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
}
}
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
public void UpdateNewEpisodes(){
int count = 0;
bool foundWatched = false;
// Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){
continue;
}
// Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue;
}
if (!Seasons[i].EpisodesList[j].WasDownloaded){
count++;
} else{
foundWatched = true;
}
}
}
NewEpisodes = count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(NewEpisodes)));
}
public void SetFetchingData(){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
}
public async Task AddNewMissingToDownloads(){
bool foundWatched = false;
// Iterate over the Seasons list from the end to the beginning
for (int i = Seasons.Count - 1; i >= 0 && !foundWatched; i--){
if (Seasons[i].SpecialSeason == true){
continue;
}
// Iterate over the Episodes from the end to the beginning
for (int j = Seasons[i].EpisodesList.Count - 1; j >= 0 && !foundWatched; j--){
if (Seasons[i].EpisodesList[j].SpecialEpisode){
continue;
}
if (!Seasons[i].EpisodesList[j].WasDownloaded){
//ADD to download queue
await Seasons[i].EpisodesList[j].DownloadEpisode();
} else{
foundWatched = true;
}
}
}
}
public async Task FetchData(string? seasonId){
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await Crunchyroll.Instance.CrHistory.UpdateSeries(SeriesId, seasonId);
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(false, this);
UpdateNewEpisodes();
}
public void RemoveSeason(string? season){
HistorySeason? objectToRemove = Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null;
if (objectToRemove != null){
Seasons.Remove(objectToRemove);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Seasons)));
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
public void OpenSonarrPage(){
var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties;
if (sonarrProp == null) return;
Helpers.OpenUrl($"http{(sonarrProp.UseSsl ? "s" : "")}://{sonarrProp.Host}:{sonarrProp.Port}{(sonarrProp.UrlBase ?? "")}/series/{SonarrSlugTitle}");
}
public void OpenCrPage(){
Helpers.OpenUrl($"https://www.crunchyroll.com/series/{SeriesId}");
}
}

View File

@ -0,0 +1,21 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
using CRD.Downloader;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI;
public class UiSonarrIdToVisibilityConverter : IValueConverter{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture){
if (value is string stringValue){
return Crunchyroll.Instance.CrunOptions.SonarrProperties != null && (stringValue.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled);
}
return false;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
throw new NotImplementedException("This converter only works for one-way binding");
}
}

View File

@ -19,17 +19,30 @@ using ReactiveUI;
namespace CRD.ViewModels; namespace CRD.ViewModels;
public partial class AddDownloadPageViewModel : ViewModelBase{ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty] public string _urlInput = ""; [ObservableProperty]
[ObservableProperty] public string _buttonText = "Enter Url"; public string _urlInput = "";
[ObservableProperty] public bool _addAllEpisodes = false;
[ObservableProperty]
public string _buttonText = "Enter Url";
[ObservableProperty]
public bool _addAllEpisodes = false;
[ObservableProperty]
public bool _buttonEnabled = false;
[ObservableProperty]
public bool _allButtonEnabled = false;
[ObservableProperty]
public bool _showLoading = false;
[ObservableProperty] public bool _buttonEnabled = false;
[ObservableProperty] public bool _allButtonEnabled = false;
[ObservableProperty] public bool _showLoading = false;
public ObservableCollection<ItemModel> Items{ get; } = new(); public ObservableCollection<ItemModel> Items{ get; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; } = new(); public ObservableCollection<ItemModel> SelectedItems{ get; } = new();
[ObservableProperty] public ComboBoxItem _currentSelectedSeason; [ObservableProperty]
public ComboBoxItem _currentSelectedSeason;
public ObservableCollection<ComboBoxItem> SeasonList{ get; } = new(); public ObservableCollection<ComboBoxItem> SeasonList{ get; } = new();
private Dictionary<string, List<ItemModel>> episodesBySeason = new(); private Dictionary<string, List<ItemModel>> episodesBySeason = new();
@ -79,7 +92,6 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (currentSeriesList != null){ if (currentSeriesList != null){
Crunchyroll.Instance.AddSeriesToQueue(currentSeriesList.Value, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, AddAllEpisodes, false, selectedEpisodes)); Crunchyroll.Instance.AddSeriesToQueue(currentSeriesList.Value, new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, AddAllEpisodes, false, selectedEpisodes));
} }
@ -106,7 +118,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (match.Success){ if (match.Success){
var locale = match.Groups[1].Value; // Capture the locale part var locale = match.Groups[1].Value; // Capture the locale part
var id = match.Groups[2].Value; // Capture the ID part var id = match.Groups[2].Value; // Capture the ID part
Crunchyroll.Instance.AddEpisodeToQue(id, locale, Crunchyroll.Instance.CrunOptions.DubLang); Crunchyroll.Instance.AddEpisodeToQue(id, locale, Crunchyroll.Instance.CrunOptions.DubLang);
UrlInput = ""; UrlInput = "";
selectedEpisodes.Clear(); selectedEpisodes.Clear();
@ -122,7 +134,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (match.Success){ if (match.Success){
var locale = match.Groups[1].Value; // Capture the locale part var locale = match.Groups[1].Value; // Capture the locale part
var id = match.Groups[2].Value; // Capture the ID part var id = match.Groups[2].Value; // Capture the ID part
if (id.Length != 9){ if (id.Length != 9){
return; return;
@ -130,7 +142,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false; ButtonEnabled = false;
ShowLoading = true; ShowLoading = true;
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id,"", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true)); var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
ShowLoading = false; ShowLoading = false;
if (list != null){ if (list != null){
currentSeriesList = list; currentSeriesList = list;

View File

@ -1,18 +1,13 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -27,64 +22,23 @@ public partial class DownloadsPageViewModel : ViewModelBase{
public bool _removeFinished; public bool _removeFinished;
public DownloadsPageViewModel(){ public DownloadsPageViewModel(){
UpdateListItems(); Crunchyroll.Instance.UpdateDownloadListItems();
Items = Crunchyroll.Instance.DownloadItemModels; Items = Crunchyroll.Instance.DownloadItemModels;
AutoDownload = Crunchyroll.Instance.CrunOptions.AutoDownload; AutoDownload = Crunchyroll.Instance.CrunOptions.AutoDownload;
RemoveFinished = Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload; RemoveFinished = Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload;
Crunchyroll.Instance.Queue.CollectionChanged += UpdateItemListOnRemove;
// Items.Add(new DownloadItemModel{Title = "Test - S1E1"});
}
private void UpdateItemListOnRemove(object? sender, NotifyCollectionChangedEventArgs e){
if (e.Action == NotifyCollectionChangedAction.Remove){
if (e.OldItems != null)
foreach (var eOldItem in e.OldItems){
var downloadItem = Crunchyroll.Instance.DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(eOldItem));
if (downloadItem != null){
Crunchyroll.Instance.DownloadItemModels.Remove(downloadItem);
} else{
Console.Error.WriteLine("Failed to Remove Episode from list");
}
}
}
UpdateListItems();
}
public void UpdateListItems(){
var list = Crunchyroll.Instance.Queue;
foreach (CrunchyEpMeta crunchyEpMeta in list){
var downloadItem = Crunchyroll.Instance.DownloadItemModels.FirstOrDefault(e => e.epMeta.Equals(crunchyEpMeta));
if (downloadItem != null){
downloadItem.Refresh();
} else{
downloadItem = new DownloadItemModel(crunchyEpMeta);
downloadItem.LoadImage();
Crunchyroll.Instance.DownloadItemModels.Add(downloadItem);
}
if (downloadItem is{ isDownloading: false, Error: false } && Crunchyroll.Instance.CrunOptions.AutoDownload && Crunchyroll.Instance.ActiveDownloads < Crunchyroll.Instance.CrunOptions.SimultaneousDownloads){
downloadItem.StartDownload();
}
}
} }
partial void OnAutoDownloadChanged(bool value){ partial void OnAutoDownloadChanged(bool value){
Crunchyroll.Instance.CrunOptions.AutoDownload = value; Crunchyroll.Instance.CrunOptions.AutoDownload = value;
if (value){ if (value){
UpdateListItems(); Crunchyroll.Instance.UpdateDownloadListItems();
} }
} }
partial void OnRemoveFinishedChanged(bool value){ partial void OnRemoveFinishedChanged(bool value){
Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload = value; Crunchyroll.Instance.CrunOptions.RemoveFinishedDownload = value;
} }
public void Cleanup(){
Crunchyroll.Instance.Queue.CollectionChanged -= UpdateItemListOnRemove;
}
} }
public partial class DownloadItemModel : INotifyPropertyChanged{ public partial class DownloadItemModel : INotifyPropertyChanged{

View File

@ -1,11 +1,21 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
using DynamicData;
using ReactiveUI; using ReactiveUI;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -22,9 +32,84 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
public static bool _editMode; public static bool _editMode;
[ObservableProperty]
public double _scaleValue;
[ObservableProperty]
public ComboBoxItem _selectedView;
public ObservableCollection<ComboBoxItem> ViewsList{ get; } =[];
[ObservableProperty]
public ComboBoxItem _selectedSorting;
public ObservableCollection<ComboBoxItem> SortingList{ get; } =[];
[ObservableProperty]
public double _posterWidth;
[ObservableProperty]
public double _posterHeight;
[ObservableProperty]
public double _posterImageWidth;
[ObservableProperty]
public double _posterImageHeight;
[ObservableProperty]
public double _posterTextSize;
[ObservableProperty]
public Thickness _cornerMargin;
private HistoryViewType currentViewType = HistoryViewType.Posters;
[ObservableProperty]
public bool _isPosterViewSelected = false;
[ObservableProperty]
public bool _isTableViewSelected = false;
[ObservableProperty]
public static bool _viewSelectionOpen;
[ObservableProperty]
public static bool _sortingSelectionOpen;
private IStorageProvider _storageProvider;
private SortingType currentSortingType = SortingType.NextAirDate;
public HistoryPageViewModel(){ public HistoryPageViewModel(){
Items = Crunchyroll.Instance.HistoryList; Items = Crunchyroll.Instance.HistoryList;
HistoryPageProperties? properties = Crunchyroll.Instance.CrunOptions.HistoryPageProperties;
currentViewType = properties?.SelectedView ?? HistoryViewType.Posters;
currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle;
ScaleValue = properties?.ScaleValue ?? 0.73;
foreach (HistoryViewType viewType in Enum.GetValues(typeof(HistoryViewType))){
var combobox = new ComboBoxItem{ Content = viewType };
ViewsList.Add(combobox);
if (viewType == currentViewType){
SelectedView = combobox;
}
}
foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){
var combobox = new ComboBoxItem{ Content = sortingType.GetEnumMemberValue() };
SortingList.Add(combobox);
if (sortingType == currentSortingType){
SelectedSorting = combobox;
}
}
IsPosterViewSelected = currentViewType == HistoryViewType.Posters;
IsTableViewSelected = currentViewType == HistoryViewType.Table;
foreach (var historySeries in Items){ foreach (var historySeries in Items){
if (historySeries.ThumbnailImage == null){ if (historySeries.ThumbnailImage == null){
historySeries.LoadImage(); historySeries.LoadImage();
@ -32,13 +117,87 @@ public partial class HistoryPageViewModel : ViewModelBase{
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
} }
Crunchyroll.Instance.CrHistory.SortItems();
} }
private void UpdateSettings(){
if (Crunchyroll.Instance.CrunOptions.HistoryPageProperties != null){
Crunchyroll.Instance.CrunOptions.HistoryPageProperties.ScaleValue = ScaleValue;
Crunchyroll.Instance.CrunOptions.HistoryPageProperties.SelectedView = currentViewType;
Crunchyroll.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
} else{
Crunchyroll.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties(){ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType };
}
CfgManager.WriteSettingsToFile();
}
partial void OnSelectedViewChanged(ComboBoxItem value){
if (Enum.TryParse(value.Content + "", out HistoryViewType viewType)){
currentViewType = viewType;
IsPosterViewSelected = currentViewType == HistoryViewType.Posters;
IsTableViewSelected = currentViewType == HistoryViewType.Table;
} else{
Console.Error.WriteLine("Invalid viewtype selected");
}
ViewSelectionOpen = false;
UpdateSettings();
}
partial void OnSelectedSortingChanged(ComboBoxItem value){
if (TryParseEnum<SortingType>(value.Content + "", out var sortingType)){
currentSortingType = sortingType;
Crunchyroll.Instance.CrHistory.SortItems();
} else{
Console.Error.WriteLine("Invalid viewtype selected");
}
SortingSelectionOpen = false;
UpdateSettings();
}
private bool TryParseEnum<T>(string value, out T result) where T : struct, Enum{
foreach (var field in typeof(T).GetFields()){
var attribute = field.GetCustomAttribute<EnumMemberAttribute>();
if (attribute != null && attribute.Value == value){
result = (T)field.GetValue(null);
return true;
}
}
result = default;
return false;
}
partial void OnScaleValueChanged(double value){
double t = (ScaleValue - 0.5) / (1 - 0.5);
PosterHeight = Math.Clamp(225 + t * (410 - 225), 225, 410);
PosterWidth = 250 * ScaleValue;
PosterImageHeight = 360 * ScaleValue;
PosterImageWidth = 240 * ScaleValue;
double posterTextSizeCalc = 11 + t * (15 - 11);
PosterTextSize = Math.Clamp(posterTextSizeCalc, 11, 15);
CornerMargin = new Thickness(0, 0, Math.Clamp(3 + t * (5 - 3), 3, 5), 0);
UpdateSettings();
}
partial void OnSelectedSeriesChanged(HistorySeries value){ partial void OnSelectedSeriesChanged(HistorySeries value){
Crunchyroll.Instance.SelectedSeries = value; Crunchyroll.Instance.SelectedSeries = value;
if (!string.IsNullOrEmpty(value.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties is{ SonarrEnabled: true }){
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true, SelectedSeries);
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
NavToSeries(); NavToSeries();
_selectedSeries = null; _selectedSeries = null;
} }
@ -81,6 +240,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
FetchingData = false; FetchingData = false;
RaisePropertyChanged(nameof(FetchingData)); RaisePropertyChanged(nameof(FetchingData));
Crunchyroll.Instance.CrHistory.SortItems();
} }
[RelayCommand] [RelayCommand]
@ -89,4 +249,65 @@ public partial class HistoryPageViewModel : ViewModelBase{
await Items[i].AddNewMissingToDownloads(); await Items[i].AddNewMissingToDownloads();
} }
} }
[RelayCommand]
public async Task OpenFolderDialogAsyncSeason(HistorySeason? season){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
// Do something with the selected folder path
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
if (season != null){
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
[RelayCommand]
public async Task OpenFolderDialogAsyncSeries(HistorySeries? series){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
// Do something with the selected folder path
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
if (series != null){
series.SeriesDownloadPath = selectedFolder.Path.LocalPath;
}
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
}
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
}
public class HistoryPageProperties(){
public SortingType? SelectedSorting{ get; set; }
public HistoryViewType SelectedView{ get; set; }
public double? ScaleValue{ get; set; }
} }

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -7,6 +8,8 @@ using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
using ReactiveUI; using ReactiveUI;
@ -30,14 +33,13 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (_selectedSeries.ThumbnailImage == null){ if (_selectedSeries.ThumbnailImage == null){
_selectedSeries.LoadImage(); _selectedSeries.LoadImage();
} }
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){ if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId) && Crunchyroll.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0; SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && Crunchyroll.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
Crunchyroll.Instance.CrHistory.MatchHistoryEpisodesWithSonarr(true,SelectedSeries); }else{
CfgManager.WriteJsonToFile(CfgManager.PathCrHistory, Crunchyroll.Instance.HistoryList);
} else{
SonarrAvailable = false; SonarrAvailable = false;
} }
} }
@ -72,23 +74,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider)); _storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
} }
[RelayCommand]
public void OpenSonarrPage(){
var sonarrProp = Crunchyroll.Instance.CrunOptions.SonarrProperties;
if (sonarrProp == null) return;
OpenUrl($"http{(sonarrProp.UseSsl ? "s" : "")}://{sonarrProp.Host}:{sonarrProp.Port}{(sonarrProp.UrlBase ?? "")}/series/{SelectedSeries.SonarrSlugTitle}");
}
[RelayCommand]
public void OpenCrPage(){
OpenUrl($"https://www.crunchyroll.com/series/{SelectedSeries.SeriesId}");
}
[RelayCommand] [RelayCommand]
public async Task UpdateData(string? season){ public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season); await SelectedSeries.FetchData(season);
@ -98,7 +84,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
[RelayCommand] [RelayCommand]
public void RemoveSeason(string? season){ public void RemoveSeason(string? season){
HistorySeason? objectToRemove = SelectedSeries.Seasons.Find(se => se.SeasonId == season) ?? null; HistorySeason? objectToRemove = SelectedSeries.Seasons.FirstOrDefault(se => se.SeasonId == season) ?? null;
if (objectToRemove != null){ if (objectToRemove != null){
SelectedSeries.Seasons.Remove(objectToRemove); SelectedSeries.Seasons.Remove(objectToRemove);
} }
@ -115,14 +101,5 @@ public partial class SeriesPageViewModel : ViewModelBase{
} }
private void OpenUrl(string url){
try{
Process.Start(new ProcessStartInfo{
FileName = url,
UseShellExecute = true
});
} catch (Exception e){
Console.Error.WriteLine($"An error occurred while trying to open URL - {url} : {e.Message}");
}
}
} }

View File

@ -124,8 +124,9 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" Text="{Binding SeasonName}" <TextBlock HorizontalAlignment="Center" TextAlignment="Center" Text="{Binding SeasonName}"
TextWrapping="NoWrap" TextWrapping="Wrap"
Height="40"
Margin="0,0,0,0"> Margin="0,0,0,0">
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="{Binding SeasonName}" FontSize="15" /> <TextBlock Text="{Binding SeasonName}" FontSize="15" />

View File

@ -10,15 +10,4 @@ public partial class DownloadsPageView : UserControl{
public DownloadsPageView(){ public DownloadsPageView(){
InitializeComponent(); InitializeComponent();
} }
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e){
base.OnDetachedFromVisualTree(e);
if (DataContext is DownloadsPageViewModel vm){
vm.Cleanup();
}
}
private void Button_OnClick(object? sender, RoutedEventArgs e){
// Crunchy.Instance.TestMethode();
}
} }

View File

@ -5,28 +5,136 @@
xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:ui="clr-namespace:CRD.Utils.UI" xmlns:ui="clr-namespace:CRD.Utils.UI"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:history="clr-namespace:CRD.Utils.Structs.History"
x:DataType="vm:HistoryPageViewModel" x:DataType="vm:HistoryPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.HistoryPageView"> x:Class="CRD.Views.HistoryPageView">
<UserControl.Resources> <UserControl.Resources>
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" /> <ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal" Margin="20 0 0 0 "> <Grid.ColumnDefinitions>
<Button Command="{Binding RefreshAll}" Margin="10" IsEnabled="{Binding !FetchingData}">Refresh All</Button> <ColumnDefinition Width="Auto" /> <!-- Takes up most space for the title -->
<Button Command="{Binding AddMissingToQueue}" Margin="10" IsEnabled="{Binding !FetchingData}">Add To Queue</Button> <ColumnDefinition Width="*" />
<ToggleButton IsChecked="{Binding EditMode}" Margin="10" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Background="#2a2a2a"></StackPanel>
<StackPanel Grid.Row="0" Grid.Column="0" Orientation="Horizontal">
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
Command="{Binding RefreshAll}"
IsEnabled="{Binding !FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sync" FontSize="32" />
<TextBlock Text="Refresh Filtered" TextWrapping="Wrap" FontSize="12"></TextBlock>
</StackPanel>
</Button>
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
Command="{Binding AddMissingToQueue}"
IsEnabled="{Binding !FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Import" FontSize="32" />
<TextBlock Text="Add To Queue" TextWrapping="Wrap" FontSize="12"></TextBlock>
</StackPanel>
</Button>
<ToggleButton Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsChecked="{Binding EditMode}"
IsEnabled="{Binding !FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Edit" FontSize="32" />
<TextBlock Text="Edit" TextWrapping="Wrap" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<!-- <Button Command="{Binding RefreshAll}" Margin="10" IsEnabled="{Binding !FetchingData}">Refresh All</Button> -->
<!-- <Button Command="{Binding AddMissingToQueue}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Add To Queue</Button> -->
<!-- <ToggleButton IsChecked="{Binding EditMode}" Margin="10 0" IsEnabled="{Binding !FetchingData}">Edit</ToggleButton> -->
</StackPanel> </StackPanel>
<ListBox Grid.Row="1" Grid.Column="0" ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedSeries}" Margin="5"> <StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
<Slider VerticalAlignment="Center" Minimum="0.5" Maximum="1" Width="100"
Value="{Binding ScaleValue}"
IsVisible="{Binding IsPosterViewSelected}">
</Slider>
<StackPanel>
<ToggleButton x:Name="DropdownButtonViews" Width="70" Height="70" Background="Transparent"
BorderThickness="0" Margin="5 0" VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}"
IsChecked="{Binding ViewSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="View" FontSize="32" />
<TextBlock Text="View" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonViews, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonViews}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding ViewsList}" SelectedItem="{Binding SelectedView}">
</ListBox>
</Border>
</Popup>
</StackPanel>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSorting" Width="70" Height="70" Background="Transparent"
BorderThickness="0" Margin="5 0" VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}"
IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="32" />
<TextBlock Text="Sort" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonSorting, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonSorting}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox SelectionMode="Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding SortingList}" SelectedItem="{Binding SelectedSorting}">
</ListBox>
</Border>
</Popup>
</StackPanel>
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsEnabled="False">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock>
</StackPanel>
</Button>
</StackPanel>
<ListBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" ItemsSource="{Binding Items}"
SelectedItem="{Binding SelectedSeries}"
Margin="5" IsVisible="{Binding IsPosterViewSelected}">
<ListBox.ItemsPanel> <ListBox.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
@ -35,32 +143,68 @@
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Grid> <Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center"
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="250" Width="250" Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterWidth}"
MaxHeight="400" Height="400" Margin="5"> Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterHeight}"
Margin="5">
<Grid> <Grid>
<Image Source="{Binding ThumbnailImage}" Width="240" Height="360"></Image> <Image Source="{Binding ThumbnailImage}"
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right" IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}"> Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
<TextBlock VerticalAlignment="Center" TextAlignment="Center" Margin="0 0 5 0" Width="30" Height="30" Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageHeight}">
Background="Black" Opacity="0.8" Text="{Binding NewEpisodes}" </Image>
Padding="0,5,0,0"/>
</StackPanel> <Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 0 10"
Background="#f78c25" Opacity="1"
Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} "
IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}">
<TextBlock VerticalAlignment="Center" TextAlignment="Center" Width="30"
Height="30"
Text="{Binding NewEpisodes}"
Padding="0,5,0,0" />
</Border>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" Text="{Binding SeriesTitle}" TextWrapping="NoWrap" MaxWidth="240"
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding SeriesTitle}"
TextWrapping="Wrap"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
Height="35"
Margin="4,0,4,0"> Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SeriesTitle}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding SonarrNextAirDate}"
TextWrapping="Wrap"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
MaxHeight="20"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SonarrNextAirDate}" FontSize="15" />
</ToolTip.Tip>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" MaxWidth="250" Width="250"
MaxHeight="400" Height="400" Background="#90000000" IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).EditMode}"> <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).EditMode}">
<Button MaxWidth="250" Width="250" MaxHeight="400" Height="400" FontStyle="Italic"
VerticalAlignment="Center" <Button
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}" Background="Transparent"
CommandParameter="{Binding SeriesId}" BorderThickness="0"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}" Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterWidth}"
> Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterHeight}"
FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}">
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" /> <TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
@ -68,22 +212,351 @@
<controls:SymbolIcon Symbol="Delete" FontSize="32" /> <controls:SymbolIcon Symbol="Delete" FontSize="32" />
</StackPanel> </StackPanel>
</Button> </Button>
</StackPanel> </Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center" MaxWidth="250" Width="250" <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding FetchingData}">
MaxHeight="400" Height="400" Background="#90000000" IsVisible="{Binding FetchingData}">
<!-- <ProgressBar IsIndeterminate="{Binding FetchingData}" -->
<!-- MaxWidth="100"> -->
<!-- </ProgressBar> -->
<controls:ProgressRing Width="100" Height="100"></controls:ProgressRing> <controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
</StackPanel> </Grid>
</Grid> </Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
<Grid Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" IsVisible="{Binding IsTableViewSelected}">
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Items}"
IsEnabled="{Binding !FetchingData}"
Margin="5" IsVisible="{Binding IsTableViewSelected}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid>
<controls:SettingsExpander
Header="{Binding .}"
ItemsSource="{Binding .}"
IsExpanded="False">
<controls:SettingsExpander.HeaderTemplate>
<DataTemplate DataType="{x:Type history:HistorySeries}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="1" Grid.Column="0">
<!-- Define a row with auto height to match the image height -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Define columns if needed -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Image element -->
<Image Margin="10"
Source="{Binding ThumbnailImage}"
Width="120"
Height="180" />
<!-- Border element aligned within the same grid cell, overlaying the image -->
<Border VerticalAlignment="Top" HorizontalAlignment="Right"
CornerRadius="0 0 0 10"
Background="#f78c25" Opacity="1"
Margin="10"
IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}"
Grid.Row="0" Grid.Column="0">
<TextBlock VerticalAlignment="Center" TextAlignment="Center"
Width="30" Height="30"
Text="{Binding NewEpisodes}"
Padding="0,5,0,0" />
</Border>
</Grid>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}"></TextBlock>
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
Text="{Binding SeriesDescription}">
</TextBlock>
<StackPanel Grid.Row="3" Orientation="Vertical">
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
<Button Width="34" Height="34" Margin="0 0 10 0"
Background="Transparent"
BorderThickness="0" CornerRadius="50"
Command="{Binding OpenCrPage}">
<Grid>
<controls:ImageIcon
Source="../Assets/crunchy_icon_round.png"
Width="30"
Height="30" />
</Grid>
</Button>
<Button Width="34" Height="34" Margin="0 0 10 0"
Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}"
Command="{Binding OpenSonarrPage}">
<Grid>
<controls:ImageIcon Source="../Assets/sonarr.png"
Width="30" Height="30" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding FetchData}" Margin="0 0 5 10">Fetch Series</Button>
<ToggleButton x:Name="SeriesEditModeToggle" IsChecked="{Binding EditModeEnabled}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsyncSeries}"
CommandParameter="{Binding .}">
<ToolTip.Tip>
<TextBlock Text="{Binding SeriesDownloadPath}"
FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</Grid>
</Grid>
</DataTemplate>
</controls:SettingsExpander.HeaderTemplate>
<controls:SettingsExpanderItem>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Seasons}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type history:HistorySeason}">
<controls:SettingsExpander
Header="{Binding CombinedProperty}"
ItemsSource="{Binding EpisodesList}"
Description="{Binding SeasonTitle}"
IsExpanded="False">
<controls:SettingsExpander.ItemTemplate>
<DataTemplate DataType="{x:Type history:HistoryEpisode}">
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center"
Margin="0 0 5 0"
IsVisible="{Binding $parent[ItemsControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<controls:ImageIcon
IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png"
Width="25"
Height="25" />
<controls:ImageIcon
IsVisible="{Binding !SonarrHasFile}"
Source="../Assets/sonarr_inactive.png"
Width="25"
Height="25" />
</StackPanel>
<Button Width="34" Height="34"
Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding !WasDownloaded}"
Command="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext).UpdateDownloaded}"
CommandParameter="{Binding EpisodeId}">
<Grid>
<Ellipse Width="25" Height="25"
Fill="Gray" />
<controls:SymbolIcon Symbol="Checkmark"
FontSize="18" />
</Grid>
</Button>
<Button Width="34" Height="34"
Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding WasDownloaded}"
Command="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext).UpdateDownloaded}"
CommandParameter="{Binding EpisodeId}">
<Grid>
<Ellipse Width="25" Height="25"
Fill="#21a556" />
<controls:SymbolIcon Symbol="Checkmark"
FontSize="18" />
</Grid>
</Button>
<Button Margin="0 0 5 0" FontStyle="Italic"
HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download"
FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</Grid>
</DataTemplate>
</controls:SettingsExpander.ItemTemplate>
<controls:SettingsExpander.Footer>
<StackPanel Orientation="Horizontal"
VerticalAlignment="Center">
<TextBlock Text="{Binding DownloadedEpisodes}" VerticalAlignment="Center"></TextBlock>
<TextBlock Text="/" VerticalAlignment="Center"></TextBlock>
<TextBlock Text="{Binding EpisodesList.Count}" VerticalAlignment="Center"></TextBlock>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).FetchData}"
CommandParameter="{Binding SeasonId}">
<ToolTip.Tip>
<TextBlock Text="Fetch Season" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Refresh"
FontSize="18" />
</StackPanel>
</Button>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsyncSeason}"
CommandParameter="{Binding .}">
<ToolTip.Tip>
<TextBlock Text="{Binding SeasonDownloadPath}"
FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder"
FontSize="18" />
</StackPanel>
</Button>
<Button Margin="10 0 0 0" FontStyle="Italic"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}"
Command="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).RemoveSeason}"
CommandParameter="{Binding SeasonId}"
VerticalAlignment="Center">
<ToolTip.Tip>
<TextBlock Text="Remove Season" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Delete"
FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<Grid IsVisible="{Binding FetchingData}"
Background="#90000000"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<controls:ProgressRing Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<Grid IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).EditMode}"
Background="#90000000"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Button
Background="Transparent"
BorderThickness="0"
FontStyle="Italic"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}">
<ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Delete" FontSize="32" />
</StackPanel>
</Button>
</Grid>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -116,6 +116,9 @@ public partial class MainWindow : AppWindow{
break; break;
case "History": case "History":
navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel)); navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel));
if ( navView.Content is HistoryPageViewModel){
((HistoryPageViewModel)navView.Content).SetStorageProvider(StorageProvider);
}
navigationStack.Clear(); navigationStack.Clear();
navigationStack.Push(navView.Content); navigationStack.Push(navView.Content);
selectedNavVieItem = selectedItem; selectedNavVieItem = selectedItem;

View File

@ -4,8 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:ui="clr-namespace:CRD.Utils.UI" xmlns:history="clr-namespace:CRD.Utils.Structs.History"
xmlns:downloader="clr-namespace:CRD.Downloader"
x:DataType="vm:SeriesPageViewModel" x:DataType="vm:SeriesPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.SeriesPageView"> x:Class="CRD.Views.SeriesPageView">
@ -46,7 +45,7 @@
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent" <Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50" BorderThickness="0" CornerRadius="50"
Command="{Binding OpenCrPage}"> Command="{Binding SelectedSeries.OpenCrPage}">
<Grid> <Grid>
<controls:ImageIcon Source="../Assets/crunchy_icon_round.png" Width="30" Height="30" /> <controls:ImageIcon Source="../Assets/crunchy_icon_round.png" Width="30" Height="30" />
</Grid> </Grid>
@ -55,13 +54,12 @@
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent" <Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50" BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SonarrAvailable}" IsVisible="{Binding SonarrAvailable}"
Command="{Binding OpenSonarrPage}"> Command="{Binding SelectedSeries.OpenSonarrPage}">
<Grid> <Grid>
<controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" /> <controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" />
</Grid> </Grid>
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
@ -138,7 +136,7 @@
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent" <Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50" BorderThickness="0" CornerRadius="50"
IsVisible="{Binding !WasDownloaded}" IsVisible="{Binding !WasDownloaded}"
Command="{Binding $parent[controls:SettingsExpander].((downloader:HistorySeason)DataContext).UpdateDownloaded}" Command="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext).UpdateDownloaded}"
CommandParameter="{Binding EpisodeId}"> CommandParameter="{Binding EpisodeId}">
<Grid> <Grid>
<Ellipse Width="25" Height="25" Fill="Gray" /> <Ellipse Width="25" Height="25" Fill="Gray" />
@ -149,7 +147,7 @@
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent" <Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50" BorderThickness="0" CornerRadius="50"
IsVisible="{Binding WasDownloaded}" IsVisible="{Binding WasDownloaded}"
Command="{Binding $parent[controls:SettingsExpander].((downloader:HistorySeason)DataContext).UpdateDownloaded}" Command="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext).UpdateDownloaded}"
CommandParameter="{Binding EpisodeId}"> CommandParameter="{Binding EpisodeId}">
<Grid> <Grid>
<Ellipse Width="25" Height="25" Fill="#21a556" /> <Ellipse Width="25" Height="25" Fill="#21a556" />