Add - Added sorting to seasons tab

Add -  Added a download subs button to the history
Chg - Changed CDM error message to include a GitHub wiki link
Chg - Changed the style of settings tabs
Chg - Changed the date format in the seasons tab
Fix - Fixed Crunchyroll seasons numbering issue (e.g., Blue Exorcist)
Fix - Fixed window slightly moving on startup
Fix - Fixed a bug in the upcoming episodes display for the next week
This commit is contained in:
Elwador 2025-01-05 02:10:52 +01:00
parent 5c200b3199
commit 51e3102503
25 changed files with 540 additions and 205 deletions

View file

@ -153,7 +153,9 @@ public class CalendarManager{
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){ public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
await LoadAnilistUpcoming(); if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
await LoadAnilistUpcoming();
}
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){ if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
return forDate; return forDate;
@ -303,15 +305,16 @@ public class CalendarManager{
} }
} }
if (CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes){
foreach (var calendarDay in week.CalendarDays){
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
foreach (var calendarDay in week.CalendarDays){ foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date)
if (calendarDay.DateTime.Date >= DateTime.Now.Date){ .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){ calendarDay.CalendarEpisodes.Add(calendarEpisode);
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; }
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date)
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){
calendarDay.CalendarEpisodes.Add(calendarEpisode);
} }
} }
} }
@ -320,7 +323,8 @@ public class CalendarManager{
foreach (var weekCalendarDay in week.CalendarDays){ foreach (var weekCalendarDay in week.CalendarDays){
if (weekCalendarDay.CalendarEpisodes.Count > 0) if (weekCalendarDay.CalendarEpisodes.Count > 0)
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
.OrderBy(e => e.DateTime) .OrderBy(e => e.AnilistEpisode) // False first, then true
.ThenBy(e => e.DateTime)
.ThenBy(e => e.SeasonName) .ThenBy(e => e.SeasonName)
.ThenBy(e => { .ThenBy(e => {
double parsedNumber; double parsedNumber;
@ -331,9 +335,9 @@ public class CalendarManager{
} }
foreach (var day in week.CalendarDays){ // foreach (var day in week.CalendarDays){
if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList(); // if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
} // }
calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week; calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week;
@ -437,38 +441,32 @@ public class CalendarManager{
if (match.Success){ if (match.Success){
crunchyrollID = match.Groups[1].Value; crunchyrollID = match.Groups[1].Value;
calEp.CrSeriesID = crunchyrollID; AdjustReleaseTimeToHistory(calEp, crunchyrollID);
} else{
Uri uri = new Uri(url);
if (CrunchyrollManager.Instance.CrunOptions.History){ if (uri.Host == "www.crunchyroll.com"
var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollID); && uri.AbsolutePath != "/"
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)){
HttpRequestMessage getUrlRequest = new HttpRequestMessage(HttpMethod.Head, url);
if (historySeries != null){ string? finalUrl = "";
var oldestRelease = DateTime.MinValue;
foreach (var historySeriesSeason in historySeries.Seasons){
if (historySeriesSeason.EpisodesList.Any()){
var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate;
if (releaseDate.HasValue && oldestRelease < releaseDate.Value){ try{
oldestRelease = releaseDate.Value; HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest);
}
}
}
if (oldestRelease != DateTime.MinValue){ finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString();
calEp.DateTime = new DateTime( } catch (Exception ex){
calEp.DateTime.Year, Console.WriteLine($"Error: {ex.Message}");
calEp.DateTime.Month, }
calEp.DateTime.Day,
oldestRelease.Hour, Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern);
oldestRelease.Minute, if (match2.Success){
oldestRelease.Second, crunchyrollID = match2.Groups[1].Value;
calEp.DateTime.Kind
); AdjustReleaseTimeToHistory(calEp, crunchyrollID);
}
} }
} }
} else{
crunchyrollID = "";
} }
} }
@ -487,6 +485,45 @@ public class CalendarManager{
} }
} }
private static void AdjustReleaseTimeToHistory(CalendarEpisode calEp, string crunchyrollId){
calEp.CrSeriesID = crunchyrollId;
if (CrunchyrollManager.Instance.CrunOptions.History){
var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollId);
if (historySeries != null){
var oldestRelease = DateTime.MinValue;
foreach (var historySeriesSeason in historySeries.Seasons){
if (historySeriesSeason.EpisodesList.Any()){
var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate;
if (releaseDate.HasValue && oldestRelease < releaseDate.Value){
oldestRelease = releaseDate.Value;
}
}
}
if (oldestRelease != DateTime.MinValue){
var adjustedDate = new DateTime(
calEp.DateTime.Year,
calEp.DateTime.Month,
calEp.DateTime.Day,
oldestRelease.Hour,
oldestRelease.Minute,
oldestRelease.Second,
calEp.DateTime.Kind
);
if ((adjustedDate - oldestRelease).TotalDays is < 6 and > 1){
adjustedDate = oldestRelease.AddDays(7);
}
calEp.DateTime = adjustedDate;
}
}
}
}
#region Query #region Query
private string query = @"query ($weekStart: Int, $weekEnd: Int, $page: Int) { private string query = @"query ($weekStart: Int, $weekEnd: Int, $page: Int) {

View file

@ -108,6 +108,8 @@ public class CrunchyrollManager{
options.FfmpegOptions = new(); options.FfmpegOptions = new();
options.DefaultAudio = "ja-JP"; options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US"; options.DefaultSub = "en-US";
options.QualityAudio = "best";
options.QualityVideo = "best";
options.CcTag = "CC"; options.CcTag = "CC";
options.CcSubsFont = "Trebuchet MS"; options.CcSubsFont = "Trebuchet MS";
options.FsRetryTime = 5; options.FsRetryTime = 5;
@ -150,11 +152,9 @@ public class CrunchyrollManager{
Username = "???", Username = "???",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP", PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE", PreferredContentSubtitleLanguage = DefaultLocale,
HasPremium = false, HasPremium = false,
}; };
Console.WriteLine($"CDM available: {_widevine.canDecrypt}");
} }
public async Task Init(){ public async Task Init(){
@ -254,10 +254,10 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
var fileNameAndPath = CrunOptions.DownloadToTempFolder var fileNameAndPath = options.DownloadToTempFolder
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty) ? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty); : Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
if (CrunOptions is{ DlVideoOnce: false, KeepDubsSeperate: true }){ if (options is{ DlVideoOnce: false, KeepDubsSeperate: true }){
var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data); var groupByDub = Helpers.GroupByLanguageWithSubtitles(res.Data);
var mergers = new List<Merger>(); var mergers = new List<Merger>();
foreach (var keyValue in groupByDub){ foreach (var keyValue in groupByDub){
@ -277,7 +277,8 @@ public class CrunchyrollManager{
SyncTiming = options.SyncTiming, SyncTiming = options.SyncTiming,
CcTag = options.CcTag, CcTag = options.CcTag,
KeepAllVideos = true, KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription MuxDescription = options.IncludeVideoDescription,
DlVideoOnce = options.DlVideoOnce
}, },
fileNameAndPath); fileNameAndPath);
@ -293,7 +294,7 @@ public class CrunchyrollManager{
foreach (var merger in mergers){ foreach (var merger in mergers){
merger.CleanUp(); merger.CleanUp();
if (CrunOptions.IsEncodeEnabled){ if (options.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){ data.DownloadProgress = new DownloadProgress(){
IsDownloading = true, IsDownloading = true,
Percent = 100, Percent = 100,
@ -304,11 +305,11 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data); await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
} }
if (CrunOptions.DownloadToTempFolder){ if (options.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); await MoveFromTempFolder(merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
} }
} }
} else{ } else{
@ -328,7 +329,8 @@ public class CrunchyrollManager{
SyncTiming = options.SyncTiming, SyncTiming = options.SyncTiming,
CcTag = options.CcTag, CcTag = options.CcTag,
KeepAllVideos = true, KeepAllVideos = true,
MuxDescription = options.IncludeVideoDescription MuxDescription = options.IncludeVideoDescription,
DlVideoOnce = options.DlVideoOnce
}, },
fileNameAndPath); fileNameAndPath);
@ -338,7 +340,7 @@ public class CrunchyrollManager{
result.merger.CleanUp(); result.merger.CleanUp();
} }
if (CrunOptions.IsEncodeEnabled){ if (options.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){ data.DownloadProgress = new DownloadProgress(){
IsDownloading = true, IsDownloading = true,
Percent = 100, Percent = 100,
@ -349,11 +351,11 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data); await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(options.EncodingPresetName), data);
} }
if (CrunOptions.DownloadToTempFolder){ if (options.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle)); await MoveFromTempFolder(result.merger, data, options, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
} }
} }
@ -367,19 +369,19 @@ public class CrunchyrollManager{
Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "") Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "")
}; };
if (CrunOptions.RemoveFinishedDownload && !syncError){ if (options.RemoveFinishedDownload && !syncError){
QueueManager.Instance.Queue.Remove(data); QueueManager.Instance.Queue.Remove(data);
} }
} else{ } else{
Console.WriteLine("Skipping mux"); Console.WriteLine("Skipping mux");
res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume")); res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
if (CrunOptions.DownloadToTempFolder){ if (options.DownloadToTempFolder){
if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){ if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){
Console.WriteLine("Invalid or non-existent temp folder path."); Console.WriteLine("Invalid or non-existent temp folder path.");
} else{ } else{
// Move files // Move files
foreach (var downloadedMedia in res.Data){ foreach (var downloadedMedia in res.Data){
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR); await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
} }
} }
} }
@ -393,7 +395,7 @@ public class CrunchyrollManager{
Doing = "Done - Skipped muxing" Doing = "Done - Skipped muxing"
}; };
if (CrunOptions.RemoveFinishedDownload){ if (options.RemoveFinishedDownload){
QueueManager.Instance.Queue.Remove(data); QueueManager.Instance.Queue.Remove(data);
} }
} }
@ -402,7 +404,7 @@ public class CrunchyrollManager{
QueueManager.Instance.ActiveDownloads--; QueueManager.Instance.ActiveDownloads--;
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
if (CrunOptions.History && data.Data != null && data.Data.Count > 0){ if (options.History && data.Data != null && data.Data.Count > 0){
History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId); History.SetAsDownloaded(data.ShowId, data.SeasonId, data.Data.First().MediaId);
} }
@ -412,8 +414,8 @@ public class CrunchyrollManager{
#region Temp Files Move #region Temp Files Move
private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, string tempFolderPath, IEnumerable<DownloadedMedia> subtitles){ private async Task MoveFromTempFolder(Merger? merger, CrunchyEpMeta data, CrDownloadOptions options, string tempFolderPath, IEnumerable<DownloadedMedia> subtitles){
if (!CrunOptions.DownloadToTempFolder) return; if (!options.DownloadToTempFolder) return;
data.DownloadProgress = new DownloadProgress{ data.DownloadProgress = new DownloadProgress{
IsDownloading = true, IsDownloading = true,
@ -431,15 +433,15 @@ public class CrunchyrollManager{
} }
// Move the main output file // Move the main output file
await MoveFile(merger?.options.Output ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR); await MoveFile(merger?.options.Output ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
// Move the subtitle files // Move the subtitle files
foreach (var downloadedMedia in subtitles){ foreach (var downloadedMedia in subtitles){
await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR); await MoveFile(downloadedMedia.Path ?? string.Empty, tempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR, options);
} }
} }
private async Task MoveFile(string sourcePath, string tempFolderPath, string downloadPath){ private async Task MoveFile(string sourcePath, string tempFolderPath, string downloadPath, CrDownloadOptions options){
if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)){ if (string.IsNullOrEmpty(sourcePath) || !File.Exists(sourcePath)){
// Console.Error.WriteLine("Source file does not exist or path is invalid."); // Console.Error.WriteLine("Source file does not exist or path is invalid.");
return; return;
@ -454,8 +456,8 @@ public class CrunchyrollManager{
var fileName = sourcePath[tempFolderPath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); var fileName = sourcePath[tempFolderPath.Length..].TrimStart(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
var destinationFolder = !string.IsNullOrEmpty(downloadPath) var destinationFolder = !string.IsNullOrEmpty(downloadPath)
? downloadPath ? downloadPath
: !string.IsNullOrEmpty(CrunOptions.DownloadDirPath) : !string.IsNullOrEmpty(options.DownloadDirPath)
? CrunOptions.DownloadDirPath ? options.DownloadDirPath
: CfgManager.PathVIDEOS_DIR; : CfgManager.PathVIDEOS_DIR;
var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName ?? string.Empty); var destinationPath = Path.Combine(destinationFolder ?? string.Empty, fileName ?? string.Empty);
@ -539,7 +541,7 @@ public class CrunchyrollManager{
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(), { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos, KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList),
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle, VideoTitle = options.VideoTitle,
Options = new MuxOptions(){ Options = new MuxOptions(){
@ -565,7 +567,7 @@ public class CrunchyrollManager{
bool isMuxed, syncError = false; bool isMuxed, syncError = false;
if (options.SyncTiming && CrunOptions.DlVideoOnce){ if (options is{ SyncTiming: true, DlVideoOnce: true }){
var basePath = merger.options.OnlyVid.First().Path; var basePath = merger.options.OnlyVid.First().Path;
var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList(); var syncVideosList = data.Where(a => a.Type == DownloadMediaType.SyncVideo).ToList();
@ -666,13 +668,13 @@ public class CrunchyrollManager{
} }
if (!_widevine.canDecrypt){ if (!_widevine.canDecrypt){
Console.Error.WriteLine("L3 key files missing"); Console.Error.WriteLine("CDM files missing");
MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page."); MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.", true);
return new DownloadResponse{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
FileName = "./unknown", FileName = "./unknown",
ErrorText = "Missing L3 Key" ErrorText = "Missing CDM files"
}; };
} }
@ -715,8 +717,8 @@ public class CrunchyrollManager{
string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId); string currentMediaId = (epMeta.MediaId.Contains(':') ? epMeta.MediaId.Split(':')[1] : epMeta.MediaId);
fileDir = CrunOptions.DownloadToTempFolder ? !string.IsNullOrEmpty(CrunOptions.DownloadTempDirPath) fileDir = options.DownloadToTempFolder ? !string.IsNullOrEmpty(options.DownloadTempDirPath)
? Path.Combine(CrunOptions.DownloadTempDirPath, Helpers.GetValidFolderName(currentMediaId)) ? Path.Combine(options.DownloadTempDirPath, Helpers.GetValidFolderName(currentMediaId))
: Path.Combine(CfgManager.PathTEMP_DIR, Helpers.GetValidFolderName(currentMediaId)) : : Path.Combine(CfgManager.PathTEMP_DIR, Helpers.GetValidFolderName(currentMediaId)) :
!string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath : !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath :
!string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR; !string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR;
@ -788,7 +790,7 @@ public class CrunchyrollManager{
#endregion #endregion
var fetchPlaybackData = await FetchPlaybackData(mediaId, mediaGuid, data.Music); var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music);
if (!fetchPlaybackData.IsOk){ if (!fetchPlaybackData.IsOk){
if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){ if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){
@ -888,7 +890,7 @@ public class CrunchyrollManager{
Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs)); Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
} }
if (dlVideoOnce && CrunOptions.DlVideoOnce){ if (dlVideoOnce && options.DlVideoOnce){
streams = streams.Where((s) => { streams = streams.Where((s) => {
if (s.HardsubLang != "-"){ if (s.HardsubLang != "-"){
return false; return false;
@ -1054,6 +1056,11 @@ public class CrunchyrollManager{
videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width)); videos.Sort((a, b) => a.quality.width.CompareTo(b.quality.width));
audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth)); audios.Sort((a, b) => a.bandwidth.CompareTo(b.bandwidth));
if (string.IsNullOrEmpty(data.VideoQuality)){
Console.Error.WriteLine("Warning: VideoQuality is null or empty. Defaulting to 'best' quality.");
data.VideoQuality = "best";
}
int chosenVideoQuality; int chosenVideoQuality;
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){ if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
chosenVideoQuality = 1; chosenVideoQuality = 1;
@ -1062,7 +1069,7 @@ public class CrunchyrollManager{
} else if (data.VideoQuality == "worst"){ } else if (data.VideoQuality == "worst"){
chosenVideoQuality = 1; chosenVideoQuality = 1;
} else{ } else{
var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality.Replace("p", "")); var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality?.Replace("p", ""));
if (tempIndex < 0){ if (tempIndex < 0){
chosenVideoQuality = videos.Count; chosenVideoQuality = videos.Count;
} else{ } else{
@ -1473,11 +1480,15 @@ public class CrunchyrollManager{
Console.WriteLine("Downloading skipped!"); Console.WriteLine("Downloading skipped!");
} }
} }
} else if (options.Novids && options.Noaudio){ } else if (options is{ Novids: true, Noaudio: true }){
variables.Add(new Variable("height", 360, false));
variables.Add(new Variable("width", 640, false));
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
} }
if (compiledChapters.Count > 0){ if (compiledChapters.Count > 0 && options is not{ Novids: true, Noaudio: true }){
try{ try{
// Parsing and constructing the file names // Parsing and constructing the file names
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
@ -1536,16 +1547,12 @@ public class CrunchyrollManager{
} else{ } else{
Console.WriteLine("Subtitles downloading skipped!"); Console.WriteLine("Subtitles downloading skipped!");
} }
} }
// await Task.Delay(options.Waittime); // await Task.Delay(options.Waittime);
} }
} }
// variables.Add(new Variable("height", quality == 0 ? plQuality.Last().RESOLUTION.Height : plQuality[quality - 1].RESOLUTION.Height, false));
// variables.Add(new Variable("width", quality == 0 ? plQuality.Last().RESOLUTION.Width : plQuality[quality - 1].RESOLUTION.Width, false));
if (options.IncludeVideoDescription){ if (options.IncludeVideoDescription){
string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml"; string fullPath = (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) + ".xml";
@ -1590,7 +1597,7 @@ public class CrunchyrollManager{
} }
var tempFolderPath = ""; var tempFolderPath = "";
if (CrunOptions.DownloadToTempFolder){ if (options.DownloadToTempFolder){
tempFolderPath = fileDir; tempFolderPath = fileDir;
fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath : fileDir = !string.IsNullOrEmpty(data.DownloadPath) ? data.DownloadPath :
!string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR; !string.IsNullOrEmpty(options.DownloadDirPath) ? options.DownloadDirPath : CfgManager.PathVIDEOS_DIR;
@ -1745,7 +1752,7 @@ public class CrunchyrollManager{
} }
File.WriteAllText(sxData.Path, subsAssReqResponse.ResponseContent); File.WriteAllText(sxData.Path, subsAssReqResponse.ResponseContent);
Console.WriteLine($"Subtitle downloaded: ${sxData.File}"); Console.WriteLine($"Subtitle downloaded: {sxData.File}");
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Subtitle, Type = DownloadMediaType.Subtitle,
Cc = isCc, Cc = isCc,
@ -1788,10 +1795,10 @@ public class CrunchyrollManager{
M3U8Json videoJson = new M3U8Json{ M3U8Json videoJson = new M3U8Json{
Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList() Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList()
}; };
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s"); data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s");
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s.resume" : $"{tsFile}.video.m4s.resume"); data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s.resume" : $"{tsFile}.video.m4s.resume");
var videoDownloader = new HlsDownloader(new HlsOptions{ var videoDownloader = new HlsDownloader(new HlsOptions{
Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s", Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s",
Timeout = options.Timeout, Timeout = options.Timeout,
@ -1844,7 +1851,7 @@ public class CrunchyrollManager{
M3U8Json audioJson = new M3U8Json{ M3U8Json audioJson = new M3U8Json{
Segments = chosenAudioSegments.segments.Cast<dynamic>().ToList() Segments = chosenAudioSegments.segments.Cast<dynamic>().ToList()
}; };
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s"); data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s");
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s.resume" : $"{tsFile}.audio.m4s.resume"); data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s.resume" : $"{tsFile}.audio.m4s.resume");
@ -1866,13 +1873,13 @@ public class CrunchyrollManager{
#region Fetch Playback Data #region Fetch Playback Data
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, bool music){ private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(CrDownloadOptions options, string mediaId, string mediaGuidId, bool music){
var temppbData = new PlaybackData{ var temppbData = new PlaybackData{
Total = 0, Total = 0,
Data = new Dictionary<string, StreamDetails>() Data = new Dictionary<string, StreamDetails>()
}; };
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{CrunOptions.StreamEndpoint}/play"; var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint); var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
if (!playbackRequestResponse.IsOk){ if (!playbackRequestResponse.IsOk){

View file

@ -302,6 +302,7 @@ public class History(){
historyEpisode.EpisodeId = crunchyEpisode.Id; historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode; historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + ""; historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
historyEpisode.EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate;
historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList); historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList);
historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales); historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales);
@ -333,8 +334,7 @@ public class History(){
newSeason.EpisodesList.Sort(new NumericStringPropertyComparer()); newSeason.EpisodesList.Sort(new NumericStringPropertyComparer());
await RefreshSeriesData(seriesId, historySeries); await RefreshSeriesData(seriesId, historySeries);
historySeries.Seasons.Add(newSeason); historySeries.Seasons.Add(newSeason);
historySeries.UpdateNewEpisodes(); historySeries.UpdateNewEpisodes();
historySeries.Init(); historySeries.Init();

View file

@ -86,7 +86,7 @@ public class QueueManager{
} }
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false){ public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
await CrunchyrollManager.Instance.CrAuth.RefreshToken(true); await CrunchyrollManager.Instance.CrAuth.RefreshToken(true);
var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale); var episodeL = await CrunchyrollManager.Instance.CrEpisode.ParseEpisodeById(epId, crLocale);
@ -100,7 +100,7 @@ public class QueueManager{
var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory); var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory);
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath,string videoQuality) historyEpisode = (null, [], [], "",""); (HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) historyEpisode = (null, [], [], "", "");
if (CrunchyrollManager.Instance.CrunOptions.History){ if (CrunchyrollManager.Instance.CrunOptions.History){
var episode = sList.EpisodeAndLanguages.Items.First(); var episode = sList.EpisodeAndLanguages.Items.First();
@ -142,9 +142,11 @@ public class QueueManager{
} }
selected.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; selected.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;
selected.OnlySubs = onlySubs;
Queue.Add(selected); Queue.Add(selected);
if (selected.Data.Count < dubLang.Count){ if (selected.Data.Count < dubLang.Count){
@ -154,7 +156,8 @@ public class QueueManager{
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) => var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray(); $"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ?? [])}]"); Console.Error.WriteLine(
$"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2)); MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue but couldn't find all selected dubs", ToastType.Warning, 2));
} else{ } else{
Console.WriteLine("Added Episode to Queue"); Console.WriteLine("Added Episode to Queue");
@ -167,7 +170,7 @@ public class QueueManager{
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) => var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray(); $"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ?? [])}]"); Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ??[])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2)); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
} }
} else{ } else{
@ -180,6 +183,7 @@ public class QueueManager{
if (movieMeta != null){ if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs; movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
movieMeta.OnlySubs = onlySubs;
Queue.Add(movieMeta); Queue.Add(movieMeta);
Console.WriteLine("Added Movie to Queue"); Console.WriteLine("Added Movie to Queue");
@ -255,7 +259,7 @@ public class QueueManager{
} }
var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId); var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId);
crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo; crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;

View file

@ -2,12 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"> xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives">
<!--
NavView style in MainView for main app navigation
While you are free to copy this into your own apps
if you want an MS store like NavView, this will NOT
be an officially supported thing in the main library
-->
<Style Selector="ui|NavigationView.SampleAppNav"> <Style Selector="ui|NavigationView.SampleAppNav">
<Setter Property="IsPaneToggleButtonVisible" Value="False" /> <Setter Property="IsPaneToggleButtonVisible" Value="False" />
<Setter Property="OpenPaneLength" Value="72" /> <Setter Property="OpenPaneLength" Value="72" />

View file

@ -69,6 +69,8 @@ public class Widevine{
Console.Error.WriteLine("Widevine: " + e); Console.Error.WriteLine("Widevine: " + e);
canDecrypt = false; canDecrypt = false;
} }
Console.WriteLine($"CDM available: {canDecrypt}");
} }
public async Task<List<ContentKey>> getKeys(string? pssh, string licenseServer, Dictionary<string, string> authData){ public async Task<List<ContentKey>> getKeys(string? pssh, string licenseServer, Dictionary<string, string> authData){

View file

@ -36,6 +36,11 @@ public class Helpers{
return default; return default;
} }
public static T DeepCopy<T>(T obj){
var json = JsonConvert.SerializeObject(obj);
return JsonConvert.DeserializeObject<T>(json);
}
public static string ConvertTimeFormat(string time){ public static string ConvertTimeFormat(string time){
var timeParts = time.Split(':', '.'); var timeParts = time.Split(':', '.');
int hours = int.Parse(timeParts[0]); int hours = int.Parse(timeParts[0]);
@ -71,7 +76,7 @@ public class Helpers{
} }
public static void EnsureDirectoriesExist(string path){ public static void EnsureDirectoriesExist(string path){
Console.WriteLine($"Check if path exists: {path}"); // Console.WriteLine($"Check if path exists: {path}");
// Check if the path is absolute // Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path); bool isAbsolute = Path.IsPathRooted(path);
@ -507,22 +512,30 @@ public class Helpers{
} }
public static string? ExtractNumberAfterS(string input){ public static string? ExtractNumberAfterS(string input){
// Regular expression pattern to match |S followed by a number and optionally C followed by another number // Regular expression pattern to match |S followed by a number and optionally C or P followed by another number
string pattern = @"\|S(\d+)(?:C(\d+))?"; string pattern = @"\|S(\d+)(?:C(\d+)|P(\d+))?";
Match match = Regex.Match(input, pattern); Match match = Regex.Match(input, pattern);
if (match.Success){ if (match.Success){
string sNumber = match.Groups[1].Value; string sNumber = match.Groups[1].Value; // Extract the S number
string cNumber = match.Groups[2].Value; string cNumber = match.Groups[2].Value; // Extract the C number if present
string pNumber = match.Groups[3].Value; // Extract the P number if present
if (!string.IsNullOrEmpty(cNumber)){ if (!string.IsNullOrEmpty(cNumber)){
// Case for C: Return S + . + C
return $"{sNumber}.{cNumber}"; return $"{sNumber}.{cNumber}";
} else if (!string.IsNullOrEmpty(pNumber)){
// Case for P: Increment S by P - 1
if (int.TryParse(sNumber, out int sNumeric) && int.TryParse(pNumber, out int pNumeric)){
return (sNumeric + (pNumeric - 1)).ToString();
}
} else{ } else{
// Return only S if no C or P is present
return sNumber; return sNumber;
} }
} else{
return null;
} }
return null;
} }

View file

@ -459,6 +459,7 @@ public class CrunchyMuxOptions{
public LanguageItem DefaultAudio{ get; set; } public LanguageItem DefaultAudio{ get; set; }
public string CcTag{ get; set; } public string CcTag{ get; set; }
public bool SyncTiming{ get; set; } public bool SyncTiming{ get; set; }
public bool DlVideoOnce{ get; set; }
} }
public class MergerOptions{ public class MergerOptions{

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -9,7 +10,7 @@ public partial class AnilistSeries : ObservableObject{
public int Id{ get; set; } public int Id{ get; set; }
public int? IdMal{ get; set; } public int? IdMal{ get; set; }
public Title Title{ get; set; } public Title Title{ get; set; }
public Date StartDate{ get; set; } public Date? StartDate{ get; set; }
public Date EndDate{ get; set; } public Date EndDate{ get; set; }
public string Status{ get; set; } public string Status{ get; set; }
public string Season{ get; set; } public string Season{ get; set; }
@ -37,13 +38,25 @@ public partial class AnilistSeries : ObservableObject{
[JsonIgnore] [JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; } public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore] [JsonIgnore]
public string StartDateForm => $"{StartDate.Day}.{StartDate.Month}.{StartDate.Year}"; public string StartDateForm{
get{
if (StartDate == null)
return string.Empty;
var cultureInfo = System.Globalization.CultureInfo.InvariantCulture;
string monthAbbreviation = cultureInfo.DateTimeFormat.GetAbbreviatedMonthName(StartDate.Month);
return string.Format("{0:00}.{1}.{2}", StartDate.Day, monthAbbreviation, StartDate.Year);
}
}
[JsonIgnore] [JsonIgnore]
public string? CrunchyrollID; public string? CrunchyrollID;
[JsonIgnore] [JsonIgnore]
[ObservableProperty] [ObservableProperty]
public bool _hasCrID; public bool _hasCrID;
@ -51,7 +64,6 @@ public partial class AnilistSeries : ObservableObject{
[JsonIgnore] [JsonIgnore]
[ObservableProperty] [ObservableProperty]
public bool _isInHistory; public bool _isInHistory;
} }
public class Title{ public class Title{
@ -61,9 +73,20 @@ public class Title{
} }
public class Date{ public class Date{
public int? Year{ get; set; } public int Year{ get; set; }
public int? Month{ get; set; } public int Month{ get; set; }
public int? Day{ get; set; } public int Day{ get; set; }
public DateTime? ToDateTime(){
if (Year == 0 || Month == 0 || Day == 0)
return DateTime.MinValue;
try{
return new DateTime(Year, Month, Day);
} catch{
return DateTime.MinValue;;
}
}
} }
public class CoverImage{ public class CoverImage{

View file

@ -80,6 +80,9 @@ public class CrDownloadOptions{
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)] [YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; } public HistoryPageProperties? HistoryPageProperties{ get; set; }
[YamlMember(Alias = "seasons_page_properties", ApplyNamingConventions = false)]
public SeasonsPageProperties? SeasonsPageProperties{ get; set; }
[YamlMember(Alias = "download_speed_limit", ApplyNamingConventions = false)] [YamlMember(Alias = "download_speed_limit", ApplyNamingConventions = false)]
public int DownloadSpeedLimit{ get; set; } public int DownloadSpeedLimit{ get; set; }
@ -225,7 +228,10 @@ public class CrDownloadOptions{
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
public bool CalendarFilterByAirDate{ get; set; } public bool CalendarFilterByAirDate{ get; set; }
[YamlMember(Alias = "calendar_show_upcoming_episodes", ApplyNamingConventions = false)]
public bool CalendarShowUpcomingEpisodes{ get; set; }
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)] [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; } public string? StreamEndpoint{ get; set; }

View file

@ -259,6 +259,8 @@ public class CrunchyEpMeta{
public string Resolution{ get; set; } public string Resolution{ get; set; }
public List<string> downloadedFiles{ get; set; } =[]; public List<string> downloadedFiles{ get; set; } =[];
public bool OnlySubs{ get; set; }
} }
public class DownloadProgress{ public class DownloadProgress{

View file

@ -9,7 +9,6 @@ using Newtonsoft.Json;
namespace CRD.Utils.Structs.History; namespace CRD.Utils.Structs.History;
public class HistoryEpisode : INotifyPropertyChanged{ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_title")] [JsonProperty("episode_title")]
public string? EpisodeTitle{ get; set; } public string? EpisodeTitle{ get; set; }
@ -24,7 +23,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_cr_season_number")] [JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; } public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_cr_premium_air_date")] [JsonProperty("episode_cr_premium_air_date")]
public DateTime? EpisodeCrPremiumAirDate{ get; set; } public DateTime? EpisodeCrPremiumAirDate{ get; set; }
@ -48,7 +47,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("sonarr_absolut_number")] [JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; } public string? SonarrAbsolutNumber{ get; set; }
[JsonProperty("history_episode_available_soft_subs")] [JsonProperty("history_episode_available_soft_subs")]
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[]; public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
@ -77,8 +76,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
public async Task DownloadEpisode(){ public async Task DownloadEpisode(bool onlySubs = false){
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId,
CrunchyrollManager.Instance.CrunOptions.DubLang); string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs);
} }
} }

View file

@ -37,6 +37,9 @@ public partial class CalendarPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _filterByAirDate; private bool _filterByAirDate;
[ObservableProperty]
private bool _showUpcomingEpisodes;
[ObservableProperty] [ObservableProperty]
private bool _hideDubs; private bool _hideDubs;
@ -79,6 +82,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar; CustomCalendar = CrunchyrollManager.Instance.CrunOptions.CustomCalendar;
HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs; HideDubs = CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs;
FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate; FilterByAirDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate;
ShowUpcomingEpisodes = CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes;
ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null; ComboBoxItem? dubfilter = CalendarDubFilter.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter) ?? null;
CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0]; CurrentCalendarDubFilter = dubfilter ?? CalendarDubFilter[0];
@ -292,6 +296,15 @@ public partial class CalendarPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value; CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate = value;
CfgManager.WriteSettingsToFile(); CfgManager.WriteSettingsToFile();
} }
partial void OnShowUpcomingEpisodesChanged(bool value){
if (loading){
return;
}
CrunchyrollManager.Instance.CrunOptions.CalendarShowUpcomingEpisodes = value;
CfgManager.WriteSettingsToFile();
}
partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){ partial void OnCurrentCalendarDubFilterChanged(ComboBoxItem? value){
if (loading){ if (loading){

View file

@ -186,7 +186,15 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
epMeta.DownloadProgress.IsDownloading = true; epMeta.DownloadProgress.IsDownloading = true;
Paused = !epMeta.Paused && !isDownloading || epMeta.Paused; Paused = !epMeta.Paused && !isDownloading || epMeta.Paused;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Paused)));
await CrunchyrollManager.Instance.DownloadEpisode(epMeta, CrunchyrollManager.Instance.CrunOptions);
CrDownloadOptions newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (epMeta.OnlySubs){
newOptions.Novids = true;
newOptions.Noaudio = true;
}
await CrunchyrollManager.Instance.DownloadEpisode(epMeta,newOptions );
} }
} }

View file

@ -108,7 +108,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public HistoryPageViewModel(){ public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance; ProgramManager = ProgramManager.Instance;
_storageProvider = ProgramManager.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.StorageProvider)); _storageProvider = ProgramManager.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.StorageProvider));
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
@ -177,8 +177,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ScaleValue = ScaleValue; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.ScaleValue = ScaleValue;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedView = currentViewType; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedView = currentViewType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.Ascending = SortDir;
} else{ } else{
CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties(){ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType }; CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties = new HistoryPageProperties()
{ ScaleValue = ScaleValue, SelectedView = currentViewType, SelectedSorting = currentSortingType, Ascending = SortDir };
} }
CfgManager.WriteSettingsToFile(); CfgManager.WriteSettingsToFile();
@ -503,6 +505,12 @@ public class HistoryPageProperties(){
public bool Ascending{ get; set; } public bool Ascending{ get; set; }
} }
public class SeasonsPageProperties(){
public SortingType? SelectedSorting{ get; set; }
public bool Ascending{ get; set; }
}
public class SortingListElement(){ public class SortingListElement(){
public SortingType SelectedSorting{ get; set; } public SortingType SelectedSorting{ get; set; }
public string? SortingTitle{ get; set; } public string? SortingTitle{ get; set; }

View file

@ -221,7 +221,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
await Task.WhenAll(downloadTasks); await Task.WhenAll(downloadTasks);
} }
[RelayCommand] [RelayCommand]
public async Task UpdateData(string? season){ public async Task UpdateData(string? season){
await SelectedSeries.FetchData(season); await SelectedSeries.FetchData(season);

View file

@ -2,6 +2,7 @@ using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll.ViewModels; using CRD.Downloader.Crunchyroll.ViewModels;
using CRD.Downloader.Crunchyroll.Views; using CRD.Downloader.Crunchyroll.Views;

View file

@ -13,6 +13,7 @@ using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
@ -154,6 +155,19 @@ public partial class UpcomingPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _isLoading; private bool _isLoading;
[ObservableProperty]
private SortingListElement? _selectedSorting;
[ObservableProperty]
private static bool _sortingSelectionOpen;
private SortingType currentSortingType;
[ObservableProperty]
private static bool _sortDir = false;
public ObservableCollection<SortingListElement> SortingList{ get; } =[];
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[]; public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[]; public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
@ -165,6 +179,23 @@ public partial class UpcomingPageViewModel : ViewModelBase{
} }
private async void LoadSeasons(){ private async void LoadSeasons(){
SeasonsPageProperties? properties = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties;
currentSortingType = properties?.SelectedSorting ?? SortingType.SeriesTitle;
SortDir = properties?.Ascending ?? false;
foreach (SortingType sortingType in Enum.GetValues(typeof(SortingType))){
if (sortingType == SortingType.HistorySeriesAddDate){
continue;
}
var combobox = new SortingListElement(){ SortingTitle = sortingType.GetEnumMemberValue(), SelectedSorting = sortingType };
SortingList.Add(combobox);
if (sortingType == currentSortingType){
SelectedSorting = combobox;
}
}
Seasons = GetTargetSeasonsAndYears(); Seasons = GetTargetSeasonsAndYears();
currentSelection = Seasons.Last(); currentSelection = Seasons.Last();
@ -175,6 +206,8 @@ public partial class UpcomingPageViewModel : ViewModelBase{
foreach (var anilistSeries in list){ foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries); SelectedSeason.Add(anilistSeries);
} }
SortItems();
} }
[RelayCommand] [RelayCommand]
@ -188,6 +221,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
foreach (var anilistSeries in list){ foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries); SelectedSeason.Add(anilistSeries);
} }
SortItems();
} }
[RelayCommand] [RelayCommand]
@ -304,7 +338,6 @@ public partial class UpcomingPageViewModel : ViewModelBase{
HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest); HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest);
finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString(); finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString();
} catch (Exception ex){ } catch (Exception ex){
Console.WriteLine($"Error: {ex.Message}"); Console.WriteLine($"Error: {ex.Message}");
} }
@ -391,4 +424,73 @@ public partial class UpcomingPageViewModel : ViewModelBase{
partial void OnSelectedSeriesChanged(AnilistSeries? value){ partial void OnSelectedSeriesChanged(AnilistSeries? value){
SelectionChangedOfSeries(value); SelectionChangedOfSeries(value);
} }
#region Sorting
private void UpdateSettings(){
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null){
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending = SortDir;
} else{
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties = new SeasonsPageProperties(){ SelectedSorting = currentSortingType, Ascending = SortDir };
}
CfgManager.WriteSettingsToFile();
}
partial void OnSelectedSortingChanged(SortingListElement? oldValue, SortingListElement? newValue){
if (newValue == null){
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null){
CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending = !CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
SortDir = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
}
Dispatcher.UIThread.InvokeAsync(() => {
SelectedSorting = oldValue ?? SortingList.First();
RaisePropertyChanged(nameof(SelectedSorting));
});
return;
}
if (newValue.SelectedSorting != null){
currentSortingType = newValue.SelectedSorting;
if (CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null) CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.SelectedSorting = currentSortingType;
SortItems();
}
SortingSelectionOpen = false;
UpdateSettings();
}
private void SortItems(){
var sortingDir = CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties != null && CrunchyrollManager.Instance.CrunOptions.SeasonsPageProperties.Ascending;
var sortedList = currentSortingType switch{
SortingType.SeriesTitle => sortingDir
? SelectedSeason
.OrderByDescending(item => item.Title.English)
.ToList()
: SelectedSeason
.OrderBy(item => item.Title.English)
.ToList(),
SortingType.NextAirDate => sortingDir
? SelectedSeason
.OrderByDescending(item => item.StartDate?.ToDateTime() ?? DateTime.MinValue)
.ThenByDescending(item => item.Title.English)
.ToList()
: SelectedSeason
.OrderBy(item => item.StartDate?.ToDateTime() ?? DateTime.MinValue)
.ThenBy(item => item.Title.English)
.ToList(),
_ => SelectedSeason.ToList()
};
SelectedSeason.Clear();
foreach (var item in sortedList){
SelectedSeason.Add(item);
}
}
#endregion
} }

View file

@ -87,10 +87,11 @@
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar Dub Filter"> <controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<TextBlock Text="Dub Filter"></TextBlock>
<ComboBox HorizontalAlignment="Center" Margin="5 0 0 5" MinWidth="200" <ComboBox HorizontalAlignment="Center" Margin="5 0 0 5" MinWidth="200"
SelectedItem="{Binding CurrentCalendarDubFilter}" SelectedItem="{Binding CurrentCalendarDubFilter}"
ItemsSource="{Binding CalendarDubFilter}"> ItemsSource="{Binding CalendarDubFilter}">
@ -98,6 +99,9 @@
<CheckBox IsChecked="{Binding FilterByAirDate}" <CheckBox IsChecked="{Binding FilterByAirDate}"
Content="Filter by episode air date" Margin="5 5 0 0"> Content="Filter by episode air date" Margin="5 5 0 0">
</CheckBox> </CheckBox>
<CheckBox IsChecked="{Binding ShowUpcomingEpisodes}"
Content="Show Upcoming episodes" Margin="5 5 0 0">
</CheckBox>
</StackPanel> </StackPanel>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>

View file

@ -104,14 +104,22 @@ public partial class MainWindow : AppWindow{
.Subscribe(message => ShowToast(message.Message, message.Type, message.Seconds)); .Subscribe(message => ShowToast(message.Message, message.Type, message.Seconds));
} }
public async void ShowError(string message){ public async void ShowError(string message,bool githubWikiButton = false){
var dialog = new ContentDialog(){ var dialog = new ContentDialog(){
Title = "Error", Title = "Error",
Content = message, Content = message,
CloseButtonText = "Close" CloseButtonText = "Close"
}; };
_ = await dialog.ShowAsync(); if (githubWikiButton){
dialog.PrimaryButtonText = "Github Wiki";
}
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){
Helpers.OpenUrl($"https://github.com/Crunchy-DL/Crunchy-Downloader/wiki");
}
} }
@ -191,7 +199,7 @@ public partial class MainWindow : AppWindow{
var screen = screens[settings.ScreenIndex]; var screen = screens[settings.ScreenIndex];
// Restore the position first // Restore the position first
Position = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment); Position = new PixelPoint(settings.PosX, settings.PosY);
// Restore the size // Restore the size
Width = settings.Width; Width = settings.Width;
@ -199,10 +207,10 @@ public partial class MainWindow : AppWindow{
// Set restore size and position for non-maximized state // Set restore size and position for non-maximized state
_restoreSize = new Size(settings.Width, settings.Height); _restoreSize = new Size(settings.Width, settings.Height);
_restorePosition = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment); _restorePosition = new PixelPoint(settings.PosX, settings.PosY);
// Ensure the window is on the correct screen before maximizing // Ensure the window is on the correct screen before maximizing
Position = new PixelPoint(settings.PosX, settings.PosY + TitleBarHeightAdjustment); Position = new PixelPoint(settings.PosX, settings.PosY );
} }
if (settings.IsMaximized){ if (settings.IsMaximized){

View file

@ -138,51 +138,51 @@
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" --> <!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" -->
<!-- ItemsSource="{Binding CrunchyrollManager.VideoQualityList}" --> <!-- ItemsSource="{Binding CrunchyrollManager.VideoQualityList}" -->
<!-- SelectedItem="{Binding SelectedSeries.SelectedVideoQuality}"> --> <!-- SelectedItem="{Binding SelectedSeries.SelectedVideoQuality}"> -->
<!-- </ComboBox> --> <!-- </ComboBox> -->
<StackPanel> <StackPanel>
<ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content> <ToggleButton.Content>
<Grid> <Grid>
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}" <TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1" <Path Grid.Column="1" Data="M 0,1 L 4,4 L 8,1" Stroke="White" StrokeThickness="1"
VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" /> VerticalAlignment="Center" Margin="5,0,5,0" Stretch="Uniform" Width="8" />
</Grid> </Grid>
</ToggleButton.Content> </ToggleButton.Content>
</ToggleButton> </ToggleButton>
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=OverrideDropdownButtonQuality, Mode=TwoWay}" IsOpen="{Binding IsChecked, ElementName=OverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom" Placement="Bottom"
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}"> PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210" <ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400" MaxHeight="400"
ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}" ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}"> SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}"> <DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock> <TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Border> </Border>
</Popup> </Popup>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
@ -342,6 +342,7 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" <StackPanel VerticalAlignment="Center" Margin="0 0 5 0"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}"> IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
@ -385,7 +386,25 @@
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="18" /> <controls:SymbolIcon Symbol="Download" FontSize="18" />
</StackPanel> </StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Episode" FontSize="15" />
</ToolTip.Tip>
</Button> </Button>
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center"
Command="{Binding DownloadEpisode}"
CommandParameter="true">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
</StackPanel>
<ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" />
</ToolTip.Tip>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
@ -488,10 +507,10 @@
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="SeasonOverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content> <ToggleButton.Content>
@ -525,10 +544,10 @@
</Border> </Border>
</Popup> </Popup>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>

View file

@ -1,4 +1,5 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity;
namespace CRD.Views; namespace CRD.Views;
@ -6,4 +7,6 @@ public partial class SeriesPageView : UserControl{
public SeriesPageView(){ public SeriesPageView(){
InitializeComponent(); InitializeComponent();
} }
} }

View file

@ -12,11 +12,26 @@
<Design.DataContext> <Design.DataContext>
<vm:SettingsPageViewModel /> <vm:SettingsPageViewModel />
</Design.DataContext> </Design.DataContext>
<controls:TabView TabItems="{Binding Tabs}" <controls:TabView TabItems="{Binding Tabs}"
AllowDropTabs="False" IsAddTabButtonVisible="False" AllowDropTabs="False" IsAddTabButtonVisible="False"
Background="Transparent" CanDragTabs="False" CanReorderTabs="False" CanDragTabs="False" CanReorderTabs="False"
VerticalAlignment="Stretch"> VerticalAlignment="Stretch">
<controls:TabView.Styles>
<Style Selector="controls|TabViewItem:selected /template/ Border#TabContainerBorder">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="controls|TabViewItem:selected /template/ Path#SelectedBackgroundPath">
<Setter Property="Fill" Value="#10FFFFFF" />
</Style>
<Style Selector="controls|TabView:not(:selected) /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
</controls:TabView.Styles>
</controls:TabView> </controls:TabView>
</UserControl> </UserControl>

View file

@ -16,8 +16,13 @@
<RowDefinition Height="*" /> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="10"> <Grid Grid.Row="0" HorizontalAlignment="Stretch" VerticalAlignment="Top" Margin="10">
<ItemsControl ItemsSource="{Binding Seasons}"> <!-- Define columns: one for centering and one for the right-aligned content -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- For centering the ItemsControl -->
<ColumnDefinition Width="Auto" /> <!-- For the settings StackPanel -->
</Grid.ColumnDefinitions>
<ItemsControl ItemsSource="{Binding Seasons}" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" /> <StackPanel Orientation="Horizontal" />
@ -45,7 +50,65 @@
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel>
<StackPanel Grid.Column="1" Margin="10" HorizontalAlignment="Right" VerticalAlignment="Center">
<StackPanel>
<ToggleButton x:Name="DropdownButtonSorting" Width="50" Height="50"
BorderThickness="0" VerticalAlignment="Center"
IsEnabled="{Binding !IsLoading}"
IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="25" />
<TextBlock Text="Sort" HorizontalAlignment="Center" 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,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SortingList}" SelectedItem="{Binding SelectedSorting}">
<ListBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Left">
<controls:SymbolIcon IsVisible="{Binding !$parent[UserControl].((vm:UpcomingPageViewModel)DataContext).SortDir}" Symbol="ChevronUp" FontSize="12" Margin="0 0 10 0" />
<controls:SymbolIcon IsVisible="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).SortDir}" Symbol="ChevronDown" FontSize="12" Margin="0 0 10 0" />
<TextBlock Text="{Binding SortingTitle}"></TextBlock>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
<!-- <ToggleButton x:Name="CalendarSettings" HorizontalContentAlignment="Stretch"> -->
<!-- <StackPanel Orientation="Horizontal"> -->
<!-- <controls:SymbolIcon Symbol="Settings" FontSize="18" /> -->
<!-- </StackPanel> -->
<!-- </ToggleButton> -->
<!-- <Popup IsLightDismissEnabled="True" -->
<!-- MaxWidth="400" -->
<!-- MaxHeight="600" -->
<!-- IsOpen="{Binding IsChecked, ElementName=CalendarSettings, Mode=TwoWay}" -->
<!-- Placement="Bottom" -->
<!-- PlacementTarget="{Binding ElementName=CalendarSettings}"> -->
<!-- <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> -->
<!-- -->
<!-- <StackPanel> -->
<!-- -->
<!-- </StackPanel> -->
<!-- -->
<!-- </Border> -->
<!-- </Popup> -->
</StackPanel>
</Grid>
<ListBox Grid.Row="1" IsVisible="{Binding !IsLoading}" ItemsSource="{Binding SelectedSeason}" <ListBox Grid.Row="1" IsVisible="{Binding !IsLoading}" ItemsSource="{Binding SelectedSeason}"
@ -88,15 +151,15 @@
</Image> </Image>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right" <StackPanel VerticalAlignment="Top" HorizontalAlignment="Right"
IsVisible="{Binding IsInHistory}" Margin="0 5 5 5" > IsVisible="{Binding IsInHistory}" Margin="0 5 5 5">
<Border Background="DarkGray" CornerRadius="50"> <Border Background="DarkGray" CornerRadius="50">
<controls:SymbolIcon Symbol="Library" Foreground="Black" FontSize="22" Margin="2" /> <controls:SymbolIcon Symbol="Library" Foreground="Black" FontSize="22" Margin="2" />
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Series is in History" FontSize="15" /> <TextBlock Text="Series is in History" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Border> </Border>
</StackPanel> </StackPanel>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center" <TextBlock HorizontalAlignment="Center" TextAlignment="Center"
@ -125,14 +188,14 @@
</StackPanel> </StackPanel>
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" > <Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right">
<Expander.Styles> <Expander.Styles>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader"> <Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="Transparent" />
</Style> </Style>
<Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader"> <Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/> <Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" /> <Setter Property="BorderBrush" Value="Transparent" />
</Style> </Style>
<Style Selector="ToggleButton:pointerover /template/ Border#ExpandCollapseChevronBorder"> <Style Selector="ToggleButton:pointerover /template/ Border#ExpandCollapseChevronBorder">
@ -146,12 +209,12 @@
</Style> </Style>
</Expander.Styles> </Expander.Styles>
<Expander.Header> <Expander.Header>
<Border Width="117" Height="315" /> <Border Width="117" Height="315" />
</Expander.Header> </Expander.Header>
<Expander.Content> <Expander.Content>
<StackPanel> <StackPanel>
<ScrollViewer MaxHeight="265" MinHeight="265" PointerWheelChanged="ScrollViewer_PointerWheelChanged" Margin="0 0 0 5"> <ScrollViewer MaxHeight="265" MinHeight="265" PointerWheelChanged="ScrollViewer_PointerWheelChanged" Margin="0 0 0 5">
<TextBlock HorizontalAlignment="Center" TextAlignment="Center" <TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding Description}" Text="{Binding Description}"
@ -162,22 +225,23 @@
</TextBlock> </TextBlock>
</ScrollViewer> </ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom"> <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0" <Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}" Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
CommandParameter="{Binding}"></Button> CommandParameter="{Binding}">
</Button>
<StackPanel IsVisible="{Binding HasCrID}"> <StackPanel IsVisible="{Binding HasCrID}">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" <Button HorizontalAlignment="Right" VerticalAlignment="Bottom"
IsVisible="{Binding !IsInHistory}" IsVisible="{Binding !IsInHistory}"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}" Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}"
CommandParameter="{Binding}"> CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Library" FontSize="20" /> <controls:SymbolIcon Symbol="Library" FontSize="20" />
<controls:SymbolIcon Symbol="Add" FontSize="20" /> <controls:SymbolIcon Symbol="Add" FontSize="20" />
</StackPanel> </StackPanel>
</Button> </Button>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Expander.Content> </Expander.Content>
@ -190,6 +254,6 @@
</ListBox> </ListBox>
<controls:ProgressRing Grid.Row="1" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="100"></controls:ProgressRing> <controls:ProgressRing Grid.Row="1" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="100"></controls:ProgressRing>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -14,7 +14,7 @@
</Design.DataContext> </Design.DataContext>
<ScrollViewer Padding="20 20 20 0"> <ScrollViewer Padding="20 20 20 0" >
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<controls:SettingsExpander Header="History" <controls:SettingsExpander Header="History"