Add - Added option to **download audio only as MP3**

Add - Added **File Name Whitespace Substitute** option in settings [#284](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/284)
Add - Added **download mode toggle (video/audio/subs)** for seasons with the ability to switch between options [#281](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/281)
Add - Added **download all** for only (video/audio/subs) for a season [#281](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/281)
Chg - Changed to **display a message** when the calendar fails to load due to Cloudflare issues [#283](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/283)
Chg - Adjusted **Calendar upcoming filter** for improved accuracy
Fix - Fixed **duplicate/wrong Crunchyroll versions** appearing in downloads [#285](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/285)
Fix - Fixed issue where **episodes with non-Japanese audio URLs** couldn't be added
Fix - Fixed **calendar crash** on Cloudflare failure [#283](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/283)
Fix - Fixed **audio-only downloads** [#279](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/279)
Fix - Fixed **crash when no featured music** is present
Fix - Fixed **"All" button not working** for music in the Add Downloads tab
Fix - Fixed that an **empty File Name Whitespace Substitute** removed all whitespaces
This commit is contained in:
Elwador 2025-06-28 09:13:28 +02:00
parent d80f9b56a0
commit 67f3d7a84a
27 changed files with 787 additions and 113 deletions

View file

@ -11,8 +11,10 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views;
using HtmlAgilityPack; using HtmlAgilityPack;
using Newtonsoft.Json; using Newtonsoft.Json;
using ReactiveUI;
namespace CRD.Downloader; namespace CRD.Downloader;
@ -75,6 +77,20 @@ public class CalendarManager{
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
response.ResponseContent.Contains("<title>Access denied</title>") ||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
response.ResponseContent.Trim().Equals("error code: 1020") ||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
MessageBus.Current.SendMessage(new ToastMessage("Blocked by Cloudflare. Use the custom calendar.", ToastType.Error, 5));
Console.Error.WriteLine($"Blocked by Cloudflare. Use the custom calendar.");
} else{
Console.Error.WriteLine($"Calendar request failed");
}
return new CalendarWeek();
}
CalendarWeek week = new CalendarWeek(); CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>(); week.CalendarDays = new List<CalendarDay>();
@ -314,8 +330,8 @@ public class CalendarManager{
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){ if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")]; var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date) foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date.Day == calendarEpisode.DateTime.Date.Day)
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){ .Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID && ele.SeasonName != calendarEpisode.SeasonName))){
calendarDay.CalendarEpisodes.Add(calendarEpisode); calendarDay.CalendarEpisodes.Add(calendarEpisode);
} }
} }

View file

@ -42,6 +42,24 @@ public class CrEpisode(){
return null; return null;
} }
if (epsidoe is{ Total: 1, Data: not null } &&
(epsidoe.Data.First().Versions ?? [])
.GroupBy(v => v.AudioLocale)
.Any(g => g.Count() > 1)){
Console.Error.WriteLine("Episode has Duplicate Audio Locales");
var list = (epsidoe.Data.First().Versions ?? []).GroupBy(v => v.AudioLocale).Where(g => g.Count() > 1).ToList();
//guid for episode id
foreach (var episodeVersionse in list){
foreach (var version in episodeVersionse){
var checkRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{version.Guid}", HttpMethod.Get, true, true, query);
var checkResponse = await HttpClientReq.Instance.SendHttpRequest(checkRequest,true);
if (!checkResponse.IsOk){
epsidoe.Data.First().Versions?.Remove(version);
}
}
}
}
if (epsidoe.Total == 1 && epsidoe.Data != null){ if (epsidoe.Total == 1 && epsidoe.Data != null){
return epsidoe.Data.First(); return epsidoe.Data.First();
} }

View file

@ -17,7 +17,7 @@ public class CrMusic{
public async Task<CrunchyMusicVideoList?> ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){ public async Task<CrunchyMusicVideoList?> ParseFeaturedMusicVideoByIdAsync(string seriesId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang); var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/featured/{seriesId}", crLocale, forcedLang);
if (musicVideos.Data is{ Count: > 0 } && updateHistory){ if (musicVideos.Data is{ Count: > 0 } && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
} }
@ -27,7 +27,7 @@ public class CrMusic{
public async Task<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){ public async Task<CrunchyMusicVideo?> ParseMusicVideoByIdAsync(string id, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos"); var musicVideo = await ParseMediaByIdAsync(id, crLocale, forcedLang, "music/music_videos");
if (musicVideo != null && updateHistory){ if (musicVideo != null && updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]); await crunInstance.History.UpdateWithMusicEpisodeList([musicVideo]);
} }
@ -39,7 +39,7 @@ public class CrMusic{
if (concert != null){ if (concert != null){
concert.EpisodeType = EpisodeType.Concert; concert.EpisodeType = EpisodeType.Concert;
if (updateHistory){ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList([concert]); await crunInstance.History.UpdateWithMusicEpisodeList([concert]);
} }
} }
@ -50,7 +50,7 @@ public class CrMusic{
public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){ public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string artistId, string crLocale, bool forcedLang = false, bool updateHistory = false){
var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang); var musicVideos = await FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{artistId}/music_videos", crLocale, forcedLang);
if (updateHistory){ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
} }
@ -66,7 +66,7 @@ public class CrMusic{
} }
} }
if (updateHistory){ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data); await crunInstance.History.UpdateWithMusicEpisodeList(concerts.Data);
} }
@ -97,7 +97,7 @@ public class CrMusic{
musicVideos.Data.AddRange(concerts.Data); musicVideos.Data.AddRange(concerts.Data);
} }
if (updateHistory){ if (updateHistory && crunInstance.CrunOptions.HistoryIncludeCrArtists){
await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data); await crunInstance.History.UpdateWithMusicEpisodeList(musicVideos.Data);
} }

View file

@ -337,6 +337,7 @@ public class CrunchyrollManager{
SkipSubMux = options.SkipSubsMux, SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}", Output = fileNameAndPath + $".{keyValue.Value.First().Lang.Locale}",
Mp4 = options.Mp4, Mp4 = options.Mp4,
Mp3 = options.AudioOnlyToMp3,
MuxFonts = options.MuxFonts, MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle, VideoTitle = res.VideoTitle,
Novids = options.Novids, Novids = options.Novids,
@ -402,6 +403,7 @@ public class CrunchyrollManager{
SkipSubMux = options.SkipSubsMux, SkipSubMux = options.SkipSubsMux,
Output = fileNameAndPath, Output = fileNameAndPath,
Mp4 = options.Mp4, Mp4 = options.Mp4,
Mp3 = options.AudioOnlyToMp3,
MuxFonts = options.MuxFonts, MuxFonts = options.MuxFonts,
VideoTitle = res.VideoTitle, VideoTitle = res.VideoTitle,
Novids = options.Novids, Novids = options.Novids,
@ -599,8 +601,10 @@ public class CrunchyrollManager{
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
if (data.FindAll(a => a.Type == DownloadMediaType.Audio).Count > 0){ if (data.FindAll(a => a.Type == DownloadMediaType.Audio).Count > 0){
Console.WriteLine("Mux to MP3"); if (options.Mp3){
muxToMp3 = true; Console.WriteLine("Mux to MP3");
muxToMp3 = true;
}
} else{ } else{
Console.WriteLine("Skip muxing since no videos are downloaded"); Console.WriteLine("Skip muxing since no videos are downloaded");
return (null, false, false, ""); return (null, false, false, "");
@ -643,9 +647,9 @@ public class CrunchyrollManager{
var merger = new Merger(new MergerOptions{ var merger = new Merger(new MergerOptions{
DubLangList = options.DubLangList, DubLangList = options.DubLangList,
SubLangList = options.SubLangList, SubLangList = options.SubLangList,
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 ,Bitrate = a.bitrate}).ToList(),
SkipSubMux = options.SkipSubMux, SkipSubMux = options.SkipSubMux,
OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty ,Bitrate = a.bitrate}).ToList(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}", Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
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(),
@ -682,7 +686,7 @@ public class CrunchyrollManager{
List<string> notSyncedDubs =[]; List<string> notSyncedDubs =[];
if (options is{ SyncTiming: true, DlVideoOnce: true }){ if (options is{ SyncTiming: true, DlVideoOnce: true } && merger.options.OnlyVid.Count > 0 && merger.options.OnlyAudio.Count > 0){
crunchyEpMeta.DownloadProgress = new DownloadProgress(){ crunchyEpMeta.DownloadProgress = new DownloadProgress(){
IsDownloading = true, IsDownloading = true,
Percent = 100, Percent = 100,
@ -916,7 +920,7 @@ public class CrunchyrollManager{
if (mediaGuid.Contains(':')){ if (mediaGuid.Contains(':')){
mediaGuid = mediaGuid.Split(':')[1]; mediaGuid = mediaGuid.Split(':')[1];
} }
Console.WriteLine("MediaGuid: " + mediaId); Console.WriteLine("MediaGuid: " + mediaId);
#region Chapters #region Chapters
@ -1154,7 +1158,7 @@ public class CrunchyrollManager{
Console.WriteLine("Downloading video..."); Console.WriteLine("Downloading video...");
curStream = streams[options.Kstream - 1]; curStream = streams[options.Kstream - 1];
Console.WriteLine($"Playlists URL: {curStream.Url} ({curStream.Type})"); Console.WriteLine($"Playlists URL: {string.Join(", ",curStream.Url)} ({curStream.Type})");
} }
string tsFile = ""; string tsFile = "";
@ -1376,7 +1380,7 @@ public class CrunchyrollManager{
Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]); Console.WriteLine("Stream URL:" + chosenVideoSegments.segments[0].uri.Split(new[]{ ",.urlset" }, StringSplitOptions.None)[0]);
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
string onlyFileName = Path.GetFileName(fileName); string onlyFileName = Path.GetFileName(fileName);
int maxLength = 220; int maxLength = 220;
@ -1391,7 +1395,7 @@ public class CrunchyrollManager{
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){ if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength); titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength);
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
onlyFileName = Path.GetFileName(fileName); onlyFileName = Path.GetFileName(fileName);
if (onlyFileName.Length > maxLength){ if (onlyFileName.Length > maxLength){
@ -1410,7 +1414,7 @@ public class CrunchyrollManager{
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale); string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.CrLocale);
string tempFile = Path.Combine(FileNameManager string tempFile = Path.Combine(FileNameManager
.ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) .ParseFileName($"temp-{(!string.IsNullOrEmpty(currentVersion.Guid) ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override)
.ToArray()); .ToArray());
string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile); string tempTsFile = Path.IsPathRooted(tempFile) ? tempFile : Path.Combine(fileDir, tempFile);
@ -1608,7 +1612,8 @@ public class CrunchyrollManager{
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang, Lang = lang,
Language = lang, Language = lang,
IsPrimary = isPrimary IsPrimary = isPrimary,
bitrate = chosenVideoSegments.bandwidth / 1024
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s"); data.downloadedFiles.Add($"{tsFile}.video.m4s");
@ -1676,7 +1681,8 @@ public class CrunchyrollManager{
Type = DownloadMediaType.Audio, Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s", Path = $"{tsFile}.audio.m4s",
Lang = lang, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary,
bitrate = chosenAudioSegments.bandwidth / 1000
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s"); data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} else{ } else{
@ -1693,7 +1699,8 @@ public class CrunchyrollManager{
Path = $"{tsFile}.video.m4s", Path = $"{tsFile}.video.m4s",
Lang = lang, Lang = lang,
Language = lang, Language = lang,
IsPrimary = isPrimary IsPrimary = isPrimary,
bitrate = chosenVideoSegments.bandwidth / 1024
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s"); data.downloadedFiles.Add($"{tsFile}.video.m4s");
@ -1704,13 +1711,14 @@ public class CrunchyrollManager{
Type = DownloadMediaType.Audio, Type = DownloadMediaType.Audio,
Path = $"{tsFile}.audio.m4s", Path = $"{tsFile}.audio.m4s",
Lang = lang, Lang = lang,
IsPrimary = isPrimary IsPrimary = isPrimary,
bitrate = chosenAudioSegments.bandwidth / 1000
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s"); data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} }
} }
} else if (options.Novids){ } else if (options.Novids){
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
Console.WriteLine("Downloading skipped!"); Console.WriteLine("Downloading skipped!");
} }
} }
@ -1718,14 +1726,14 @@ public class CrunchyrollManager{
variables.Add(new Variable("height", 360, false)); variables.Add(new Variable("height", 360, false));
variables.Add(new Variable("width", 640, 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.FileNameWhitespaceSubstitute, options.Override).ToArray());
} }
if (compiledChapters.Count > 0 && options is not{ Novids: true, Noaudio: true }){ 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.FileNameWhitespaceSubstitute, options.Override).ToArray());
var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray()); var outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).ToArray());
if (Path.IsPathRooted(outFile)){ if (Path.IsPathRooted(outFile)){
tsFile = outFile; tsFile = outFile;
} else{ } else{
@ -1847,7 +1855,7 @@ public class CrunchyrollManager{
Error = dlFailed, Error = dlFailed,
FileName = fileName.Length > 0 ? fileName : "unknown - " + Guid.NewGuid(), FileName = fileName.Length > 0 ? fileName : "unknown - " + Guid.NewGuid(),
ErrorText = "", ErrorText = "",
VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers, options.Override).Last(), VideoTitle = FileNameManager.ParseFileName(options.VideoTitle ?? "", variables, options.Numbers,options.FileNameWhitespaceSubstitute, options.Override).Last(),
FolderPath = fileDir, FolderPath = fileDir,
TempFolderPath = tempFolderPath TempFolderPath = tempFolderPath
}; };

View file

@ -60,6 +60,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private bool _muxToMp4; private bool _muxToMp4;
[ObservableProperty]
private bool _muxToMp3;
[ObservableProperty] [ObservableProperty]
private bool _muxFonts; private bool _muxFonts;
@ -93,6 +96,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private string _fileName = ""; private string _fileName = "";
[ObservableProperty]
private string _fileNameWhitespaceSubstitute = "";
[ObservableProperty] [ObservableProperty]
private string _fileTitle = ""; private string _fileTitle = "";
@ -347,11 +353,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
KeepDubsSeparate = options.KeepDubsSeperate; KeepDubsSeparate = options.KeepDubsSeperate;
DownloadChapters = options.Chapters; DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4; MuxToMp4 = options.Mp4;
MuxToMp3 = options.AudioOnlyToMp3;
MuxFonts = options.MuxFonts; MuxFonts = options.MuxFonts;
SyncTimings = options.SyncTiming; SyncTimings = options.SyncTiming;
SkipSubMux = options.SkipSubsMux; SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers; LeadingNumbers = options.Numbers;
FileName = options.FileName; FileName = options.FileName;
FileNameWhitespaceSubstitute = options.FileNameWhitespaceSubstitute;
SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic; SearchFetchFeaturedMusic = options.SearchFetchFeaturedMusic;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null; ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
@ -413,11 +421,13 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters; CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing; CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing;
CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4; CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4;
CrunchyrollManager.Instance.CrunOptions.AudioOnlyToMp3 = MuxToMp3;
CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts; CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts;
CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings; CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings;
CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux; CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10); CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10);
CrunchyrollManager.Instance.CrunOptions.FileName = FileName; CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
CrunchyrollManager.Instance.CrunOptions.FileNameWhitespaceSubstitute = FileNameWhitespaceSubstitute;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs; CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs; CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000); CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 1), 1, 10000);

View file

@ -319,6 +319,14 @@
HorizontalAlignment="Stretch" /> HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="FileName Whitespace Substitute"
Description="Character used to replace whitespace in file name variables like ${seriesTitle}">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="50"
Text="{Binding FileNameWhitespaceSubstitute}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename" <controls:SettingsExpanderItem Content="Filename"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\"> Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\">
@ -345,11 +353,17 @@
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP4" Description="Outputs a mp4 instead of a mkv - not recommended to use this option"> <controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP4" Description="Outputs an MP4 instead of an MKV — not recommended to use this option">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox> <CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
</controls:SettingsExpanderItem.Footer> </controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem> </controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="MP3" Description="Outputs an MP3 instead of an MKV/MP4 if only audio streams were downloaded">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding MuxToMp3}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate"> <controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer> <controls:SettingsExpanderItem.Footer>

View file

@ -37,8 +37,12 @@ public class History{
} }
} }
} else{ } else{
foreach (var historyEpisode in historySeries.Seasons.First(historySeason => historySeason.SeasonId == seasonId).EpisodesList){ var matchingSeason = historySeries.Seasons.FirstOrDefault(historySeason => historySeason.SeasonId == seasonId);
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
if (matchingSeason != null){
foreach (var historyEpisode in matchingSeason.EpisodesList){
historyEpisode.IsEpisodeAvailableOnStreamingService = false;
}
} }
} }
} }
@ -165,6 +169,7 @@ public class History{
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType(), EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true, IsEpisodeAvailableOnStreamingService = true,
ThumbnailImageUrl = historySource.GetImageUrl(),
}; };
historySeason.EpisodesList.Add(newHistoryEpisode); historySeason.EpisodesList.Add(newHistoryEpisode);
@ -179,6 +184,7 @@ public class History{
historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate(); historyEpisode.EpisodeCrPremiumAirDate = historySource.GetAvailableDate();
historyEpisode.EpisodeType = historySource.GetEpisodeType(); historyEpisode.EpisodeType = historySource.GetEpisodeType();
historyEpisode.IsEpisodeAvailableOnStreamingService = true; historyEpisode.IsEpisodeAvailableOnStreamingService = true;
historyEpisode.ThumbnailImageUrl = historySource.GetImageUrl();
historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang(); historyEpisode.HistoryEpisodeAvailableDubLang = historySource.GetEpisodeAvailableDubLang();
historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(); historyEpisode.HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs();
@ -577,7 +583,8 @@ public class History{
HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(), HistoryEpisodeAvailableSoftSubs = historySource.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = historySource.GetAvailableDate(), EpisodeCrPremiumAirDate = historySource.GetAvailableDate(),
EpisodeType = historySource.GetEpisodeType(), EpisodeType = historySource.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true IsEpisodeAvailableOnStreamingService = true,
ThumbnailImageUrl = historySource.GetImageUrl(),
}; };
newSeason.EpisodesList.Add(newHistoryEpisode); newSeason.EpisodesList.Add(newHistoryEpisode);
@ -634,7 +641,7 @@ public class History{
var historyEpisodesWithSonarrIds = allHistoryEpisodes var historyEpisodesWithSonarrIds = allHistoryEpisodes
.Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId)) .Where(e => !string.IsNullOrEmpty(e.SonarrEpisodeId))
.ToList(); .ToList();
Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => { Parallel.ForEach(historyEpisodesWithSonarrIds, historyEpisode => {
var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId)); var sonarrEpisode = episodes.FirstOrDefault(e => e.Id.ToString().Equals(historyEpisode.SonarrEpisodeId));
@ -644,9 +651,9 @@ public class History{
}); });
var historyEpisodeIds = new HashSet<string>(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!)); var historyEpisodeIds = new HashSet<string>(historyEpisodesWithSonarrIds.Select(e => e.SonarrEpisodeId!));
episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString())); episodes.RemoveAll(e => historyEpisodeIds.Contains(e.Id.ToString()));
allHistoryEpisodes = allHistoryEpisodes allHistoryEpisodes = allHistoryEpisodes
.Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId)) .Where(e => string.IsNullOrEmpty(e.SonarrEpisodeId))
.ToList(); .ToList();

View file

@ -95,7 +95,7 @@ public partial class QueueManager : ObservableObject{
} }
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){ public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
if (string.IsNullOrEmpty(epId)){ if (string.IsNullOrEmpty(epId)){
return; return;
} }
@ -158,7 +158,7 @@ public partial class QueueManager : ObservableObject{
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; selected.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs;
if (CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub && selected.Data.Count > 1){ if (CrunchyrollManager.Instance.CrunOptions.DownloadFirstAvailableDub && selected.Data.Count > 1){
var sortedMetaData = selected.Data var sortedMetaData = selected.Data
@ -178,9 +178,24 @@ public partial class QueueManager : ObservableObject{
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (selected.OnlySubs){ switch (episodeDownloadMode){
newOptions.Novids = true; case EpisodeDownloadMode.OnlyVideo:
newOptions.Noaudio = true; newOptions.Novids = false;
newOptions.Noaudio = true;
selected.DownloadSubs = ["none"];
break;
case EpisodeDownloadMode.OnlyAudio:
newOptions.Novids = true;
newOptions.Noaudio = false;
selected.DownloadSubs = ["none"];
break;
case EpisodeDownloadMode.OnlySubs:
newOptions.Novids = true;
newOptions.Noaudio = true;
break;
case EpisodeDownloadMode.Default:
default:
break;
} }
newOptions.DubLang = dubLang; newOptions.DubLang = dubLang;
@ -227,13 +242,28 @@ public partial class QueueManager : ObservableObject{
if (movieMeta != null){ if (movieMeta != null){
movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs; movieMeta.DownloadSubs = CrunchyrollManager.Instance.CrunOptions.DlSubs;
movieMeta.OnlySubs = onlySubs; movieMeta.OnlySubs = episodeDownloadMode == EpisodeDownloadMode.OnlySubs;
var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions); var newOptions = Helpers.DeepCopy(CrunchyrollManager.Instance.CrunOptions);
if (movieMeta.OnlySubs){ switch (episodeDownloadMode){
newOptions.Novids = true; case EpisodeDownloadMode.OnlyVideo:
newOptions.Noaudio = true; newOptions.Novids = false;
newOptions.Noaudio = true;
movieMeta.DownloadSubs = ["none"];
break;
case EpisodeDownloadMode.OnlyAudio:
newOptions.Novids = true;
newOptions.Noaudio = false;
movieMeta.DownloadSubs = ["none"];
break;
case EpisodeDownloadMode.OnlySubs:
newOptions.Novids = true;
newOptions.Noaudio = true;
break;
case EpisodeDownloadMode.Default:
default:
break;
} }
newOptions.DubLang = dubLang; newOptions.DubLang = dubLang;

View file

@ -251,6 +251,13 @@ public enum CrunchyUrlType{
Unknown Unknown
} }
public enum EpisodeDownloadMode{
Default,
OnlyVideo,
OnlyAudio,
OnlySubs,
}
public enum SonarrCoverType{ public enum SonarrCoverType{
Banner, Banner,
FanArt, FanArt,

View file

@ -9,7 +9,7 @@ using CRD.Utils.Structs;
namespace CRD.Utils.Files; namespace CRD.Utils.Files;
public class FileNameManager{ public class FileNameManager{
public static List<string> ParseFileName(string input, List<Variable> variables, int numbers, List<string> @override){ public static List<string> ParseFileName(string input, List<Variable> variables, int numbers,string whiteSpaceReplace, List<string> @override){
Regex varRegex = new Regex(@"\${[A-Za-z1-9]+}"); Regex varRegex = new Regex(@"\${[A-Za-z1-9]+}");
var matches = varRegex.Matches(input).Cast<Match>().Select(m => m.Value).ToList(); var matches = varRegex.Matches(input).Cast<Match>().Select(m => m.Value).ToList();
var overriddenVars = ParseOverride(variables, @override); var overriddenVars = ParseOverride(variables, @override);
@ -27,7 +27,7 @@ public class FileNameManager{
continue; continue;
} }
string replacement = variable.ReplaceWith.ToString(); string replacement = variable.ReplaceWith.ToString() ?? string.Empty;
if (variable.Type == "int32"){ if (variable.Type == "int32"){
int len = replacement.Length; int len = replacement.Length;
replacement = len < numbers ? new string('0', numbers - len) + replacement : replacement; replacement = len < numbers ? new string('0', numbers - len) + replacement : replacement;
@ -38,6 +38,9 @@ public class FileNameManager{
replacement = replacement.Replace(",", "."); replacement = replacement.Replace(",", ".");
} else if (variable.Sanitize){ } else if (variable.Sanitize){
replacement = CleanupFilename(replacement); replacement = CleanupFilename(replacement);
if (variable.Type == "string" && !string.IsNullOrEmpty(whiteSpaceReplace)){
replacement = replacement.Replace(" ",whiteSpaceReplace);
}
} }
input = input.Replace(match, replacement); input = input.Replace(match, replacement);

View file

@ -131,15 +131,14 @@ public class Merger{
} }
foreach (var aud in options.OnlyAudio){ if (options.OnlyAudio.Count > 1){
args.Add($"-i \"{aud.Path}\""); Console.Error.WriteLine("Multiple audio files detected. Only one audio file can be converted to MP3 at a time.");
metaData.Add($"-map {index}");
metaData.Add($"-metadata:s:a:{audioIndex} language={aud.Language.Code}");
index++;
audioIndex++;
} }
args.Add("-c:a copy"); var audio = options.OnlyAudio.First();
args.Add($"-i \"{audio.Path}\"");
args.Add("-c:a libmp3lame" + (audio.Bitrate > 0 ? $" -b:a {audio.Bitrate}k" : "") );
args.Add($"\"{options.Output}\""); args.Add($"\"{options.Output}\"");
return string.Join(" ", args); return string.Join(" ", args);
} }
@ -443,6 +442,7 @@ public class MergerInput{
public int? Duration{ get; set; } public int? Duration{ get; set; }
public int? Delay{ get; set; } public int? Delay{ get; set; }
public bool? IsPrimary{ get; set; } public bool? IsPrimary{ get; set; }
public int? Bitrate{ get; set; }
} }
public class SubtitleInput{ public class SubtitleInput{
@ -469,6 +469,7 @@ public class CrunchyMuxOptions{
public bool? KeepAllVideos{ get; set; } public bool? KeepAllVideos{ get; set; }
public bool? Novids{ get; set; } public bool? Novids{ get; set; }
public bool Mp4{ get; set; } public bool Mp4{ get; set; }
public bool Mp3{ get; set; }
public bool MuxFonts{ get; set; } public bool MuxFonts{ get; set; }
public bool MuxDescription{ get; set; } public bool MuxDescription{ get; set; }
public string ForceMuxer{ get; set; } public string ForceMuxer{ get; set; }

View file

@ -155,6 +155,9 @@ public class CrDownloadOptions{
[JsonProperty("quality_audio")] [JsonProperty("quality_audio")]
public string QualityAudio{ get; set; } = ""; public string QualityAudio{ get; set; } = "";
[JsonProperty("file_name_whitespace_substitute")]
public string FileNameWhitespaceSubstitute{ get; set; } = "";
[JsonProperty("file_name")] [JsonProperty("file_name")]
public string FileName{ get; set; } = ""; public string FileName{ get; set; } = "";
@ -194,6 +197,9 @@ public class CrDownloadOptions{
[JsonProperty("mux_mp4")] [JsonProperty("mux_mp4")]
public bool Mp4{ get; set; } public bool Mp4{ get; set; }
[JsonProperty("mux_audio_only_to_mp3")]
public bool AudioOnlyToMp3 { get; set; }
[JsonProperty("mux_fonts")] [JsonProperty("mux_fonts")]
public bool MuxFonts{ get; set; } public bool MuxFonts{ get; set; }

View file

@ -289,6 +289,14 @@ public class CrunchyEpisode : IHistorySource{
public EpisodeType GetEpisodeType(){ public EpisodeType GetEpisodeType(){
return EpisodeType; return EpisodeType;
} }
public string GetImageUrl(){
if (Images != null){
return Images.Thumbnail?.First().First().Source ?? string.Empty;
}
return string.Empty;
}
#endregion #endregion
} }

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -170,6 +171,14 @@ public class CrunchyMusicVideo : IHistorySource{
public EpisodeType GetEpisodeType(){ public EpisodeType GetEpisodeType(){
return EpisodeType; return EpisodeType;
} }
public string GetImageUrl(){
if (Images != null){
return Images.Thumbnail.First().Source ?? string.Empty;
}
return string.Empty;
}
#endregion #endregion
} }

View file

@ -84,7 +84,8 @@ public class DownloadedMedia : SxItem{
public DownloadMediaType Type{ get; set; } public DownloadMediaType Type{ get; set; }
public required LanguageItem Lang{ get; set; } public required LanguageItem Lang{ get; set; }
public bool IsPrimary{ get; set; } public bool IsPrimary{ get; set; }
public int bitrate{ get; set; }
public bool? Cc{ get; set; } public bool? Cc{ get; set; }
public bool? Signs{ get; set; } public bool? Signs{ get; set; }

View file

@ -2,6 +2,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Files; using CRD.Utils.Files;
@ -34,19 +35,22 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_special_episode")] [JsonProperty("episode_special_episode")]
public bool SpecialEpisode{ get; set; } public bool SpecialEpisode{ get; set; }
[JsonProperty("episode_available_on_streaming_service")] [JsonProperty("episode_available_on_streaming_service")]
public bool IsEpisodeAvailableOnStreamingService{ get; set; } public bool IsEpisodeAvailableOnStreamingService{ get; set; }
[JsonProperty("episode_type")] [JsonProperty("episode_type")]
public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown; public EpisodeType EpisodeType{ get; set; } = EpisodeType.Unknown;
[JsonProperty("episode_thumbnail_url")]
public string? ThumbnailImageUrl{ get; set; }
[JsonProperty("sonarr_episode_id")] [JsonProperty("sonarr_episode_id")]
public string? SonarrEpisodeId{ get; set; } public string? SonarrEpisodeId{ get; set; }
[JsonProperty("sonarr_has_file")] [JsonProperty("sonarr_has_file")]
public bool SonarrHasFile{ get; set; } public bool SonarrHasFile{ get; set; }
[JsonProperty("sonarr_is_monitored")] [JsonProperty("sonarr_is_monitored")]
public bool SonarrIsMonitored{ get; set; } public bool SonarrIsMonitored{ get; set; }
@ -70,12 +74,32 @@ public class HistoryEpisode : INotifyPropertyChanged{
return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}"; return $"S{SonarrSeasonNumber}E{SonarrEpisodeNumber}";
} }
} }
[JsonProperty("history_episode_available_soft_subs")] [JsonProperty("history_episode_available_soft_subs")]
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[]; public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_episode_available_dub_lang")] [JsonProperty("history_episode_available_dub_lang")]
public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[]; public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[];
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore]
public bool IsImageLoaded{ get; private set; } = false;
public async Task LoadImage(){
if (IsImageLoaded || string.IsNullOrEmpty(ThumbnailImageUrl))
return;
try{
ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl);
IsImageLoaded = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
} catch (Exception ex){
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
}
[JsonIgnore] [JsonIgnore]
public string ReleaseDateFormated{ public string ReleaseDateFormated{
@ -92,7 +116,7 @@ public class HistoryEpisode : INotifyPropertyChanged{
return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year); return string.Format("{0:00}.{1}.{2}", EpisodeCrPremiumAirDate.Value.Day, monthAbbreviation, EpisodeCrPremiumAirDate.Value.Year);
} }
} }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public void ToggleWasDownloaded(){ public void ToggleWasDownloaded(){
@ -115,7 +139,11 @@ public class HistoryEpisode : INotifyPropertyChanged{
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
public async Task DownloadEpisode(bool onlySubs = false){ public async Task DownloadEpisodeDefault(){
await DownloadEpisode();
}
public async Task DownloadEpisode(EpisodeDownloadMode episodeDownloadMode = EpisodeDownloadMode.Default){
switch (EpisodeType){ switch (EpisodeType){
case EpisodeType.MusicVideo: case EpisodeType.MusicVideo:
await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty); await QueueManager.Instance.CrAddMusicVideoToQueue(EpisodeId ?? string.Empty);
@ -128,19 +156,19 @@ public class HistoryEpisode : INotifyPropertyChanged{
default: default:
await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty, await QueueManager.Instance.CrAddEpisodeToQueue(EpisodeId ?? string.Empty,
string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang, string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.HistoryLang) ? CrunchyrollManager.Instance.DefaultLocale : CrunchyrollManager.Instance.CrunOptions.HistoryLang,
CrunchyrollManager.Instance.CrunOptions.DubLang, false, onlySubs); CrunchyrollManager.Instance.CrunOptions.DubLang, false, episodeDownloadMode);
break; break;
} }
} }
public void AssignSonarrEpisodeData(SonarrEpisode episode) { public void AssignSonarrEpisodeData(SonarrEpisode episode){
SonarrEpisodeId = episode.Id.ToString(); SonarrEpisodeId = episode.Id.ToString();
SonarrEpisodeNumber = episode.EpisodeNumber.ToString(); SonarrEpisodeNumber = episode.EpisodeNumber.ToString();
SonarrHasFile = episode.HasFile; SonarrHasFile = episode.HasFile;
SonarrIsMonitored = episode.Monitored; SonarrIsMonitored = episode.Monitored;
SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber.ToString(); SonarrAbsolutNumber = episode.AbsoluteEpisodeNumber.ToString();
SonarrSeasonNumber = episode.SeasonNumber.ToString(); SonarrSeasonNumber = episode.SeasonNumber.ToString();
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SonarrSeasonEpisodeText)));
} }
} }

View file

@ -10,6 +10,8 @@ public interface IHistorySource{
string GetSeasonNum(); string GetSeasonNum();
string GetSeasonId(); string GetSeasonId();
string GetImageUrl();
string GetEpisodeId(); string GetEpisodeId();
string GetEpisodeNumber(); string GetEpisodeNumber();
string GetEpisodeTitle(); string GetEpisodeTitle();

View file

@ -0,0 +1,26 @@
using System;
using System.Globalization;
using Avalonia.Data.Converters;
namespace CRD.Utils.UI;
public class UiEnumToBoolConverter : IValueConverter{
public object Convert(object? value, Type targetType, object? parameter, CultureInfo culture){
if (value == null || parameter == null)
return false;
string enumString = parameter.ToString();
if (enumString == null)
return false;
return value.ToString() == enumString;
}
public object ConvertBack(object value, Type targetType, object? parameter, CultureInfo culture){
if ((bool)value && parameter != null){
return Enum.Parse(targetType, parameter.ToString() ?? string.Empty);
}
return Avalonia.Data.BindingOperations.DoNothing;
}
}

View file

@ -208,6 +208,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
QueueManager.Instance.CrAddMusicMetaToQueue(meta); QueueManager.Instance.CrAddMusicMetaToQueue(meta);
} }
} }
} else if (AddAllEpisodes){
var musicClass = CrunchyrollManager.Instance.CrMusic;
foreach (var meta in currentMusicVideoList.Data.Select(crunchyMusicVideo => musicClass.EpisodeMeta(crunchyMusicVideo))){
QueueManager.Instance.CrAddMusicMetaToQueue(meta);
}
} }
} }
@ -363,7 +368,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
private void PopulateItemsFromMusicVideoList(){ private void PopulateItemsFromMusicVideoList(){
if (currentMusicVideoList?.Data != null){ if (currentMusicVideoList?.Data is{ Count: > 0 }){
foreach (var episode in currentMusicVideoList.Data){ foreach (var episode in currentMusicVideoList.Data){
string seasonKey; string seasonKey;
switch (episode.EpisodeType){ switch (episode.EpisodeType){
@ -394,7 +399,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
} }
CurrentSelectedSeason = SeasonList.First(); if (SeasonList.Count > 0){
CurrentSelectedSeason = SeasonList.First();
}
} }
} }

View file

@ -217,7 +217,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
var refreshDate = DateTime.Now; var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1); refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
} }
@ -239,7 +239,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
var refreshDate = DateTime.Now; var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){ if (currentWeek?.FirstDayOfWeek != null && currentWeek.FirstDayOfWeek != DateTime.MinValue){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13); refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
} }

View file

@ -20,6 +20,7 @@ using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views; using CRD.Views;
using DynamicData; using DynamicData;
using FluentAvalonia.UI.Controls;
using ReactiveUI; using ReactiveUI;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -115,6 +116,16 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private static string _progressText; private static string _progressText;
#region Table Mode
[ObservableProperty]
private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs;
[ObservableProperty]
public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
#endregion
public Vector LastScrollOffset { get; set; } = Vector.Zero; public Vector LastScrollOffset { get; set; } = Vector.Zero;
public HistoryPageViewModel(){ public HistoryPageViewModel(){
@ -528,6 +539,26 @@ public partial class HistoryPageViewModel : ViewModelBase{
await episode.DownloadEpisode(); await episode.DownloadEpisode();
} }
} }
[RelayCommand]
public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
var downloadMode = SelectedDownloadMode;
if (downloadMode != EpisodeDownloadMode.Default){
await episode.DownloadEpisode(downloadMode);
}
}
[RelayCommand]
public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){
var downloadMode = SelectedDownloadMode;
if (downloadMode != EpisodeDownloadMode.Default){
foreach (var episode in season.EpisodesList){
await episode.DownloadEpisode(downloadMode);
}
}
}
[RelayCommand] [RelayCommand]
public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){ public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){
@ -575,6 +606,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
public void ToggleInactive(){ public void ToggleInactive(){
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
SelectedDownloadIcon = SelectedDownloadMode switch{
EpisodeDownloadMode.OnlyVideo => Symbol.Video,
EpisodeDownloadMode.OnlyAudio => Symbol.Audio,
EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption,
_ => Symbol.ClosedCaption
};
}
} }
public class HistoryPageProperties{ public class HistoryPageProperties{

View file

@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
@ -32,9 +33,18 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
public static bool _showMonitoredBookmark; public static bool _showMonitoredBookmark;
[ObservableProperty]
public static bool _showFeaturedMusicButton;
[ObservableProperty] [ObservableProperty]
public static bool _sonarrConnected; public static bool _sonarrConnected;
[ObservableProperty]
private static EpisodeDownloadMode _selectedDownloadMode = EpisodeDownloadMode.OnlySubs;
[ObservableProperty]
public Symbol _selectedDownloadIcon = Symbol.ClosedCaption;
private IStorageProvider? _storageProvider; private IStorageProvider? _storageProvider;
public SeriesPageViewModel(){ public SeriesPageViewModel(){
@ -63,6 +73,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
} }
SelectedSeries.UpdateSeriesFolderPath(); SelectedSeries.UpdateSeriesFolderPath();
if (SelectedSeries.SeriesStreamingService == StreamingService.Crunchyroll && SelectedSeries.SeriesType != SeriesType.Artist){
ShowFeaturedMusicButton = true;
}
} }
@ -95,6 +109,33 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.UpdateSeriesFolderPath(); SelectedSeries.UpdateSeriesFolderPath();
} }
[RelayCommand]
public async Task OpenFeaturedMusicDialog(){
if (SelectedSeries.SeriesStreamingService != StreamingService.Crunchyroll || SelectedSeries.SeriesType == SeriesType.Artist){
return;
}
var musicList = await CrunchyrollManager.Instance.CrMusic.ParseFeaturedMusicVideoByIdAsync(SelectedSeries.SeriesId ?? string.Empty,
CrunchyrollManager.Instance.CrunOptions.HistoryLang ?? CrunchyrollManager.Instance.DefaultLocale, true, true);
if (musicList is{ Data.Count: > 0 }){
var dialog = new CustomContentDialog(){
Title = "Featured Music",
CloseButtonText = "Close",
FullSizeDesired = true
};
var viewModel = new ContentDialogFeaturedMusicViewModel(dialog, musicList, CrunchyrollManager.Instance.CrunOptions.HistoryIncludeCrArtists);
dialog.Content = new ContentDialogFeaturedMusicView(){
DataContext = viewModel
};
var dialogResult = await dialog.ShowAsync();
} else{
MessageBus.Current.SendMessage(new ToastMessage($"No featured music found", ToastType.Warning, 3));
}
}
[RelayCommand] [RelayCommand]
public async Task MatchSonarrSeries_Button(){ public async Task MatchSonarrSeries_Button(){
var dialog = new ContentDialog(){ var dialog = new ContentDialog(){
@ -151,7 +192,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
if (dialogResult == ContentDialogResult.Primary){ if (dialogResult == ContentDialogResult.Primary){
var sonarrEpisode = viewModel.CurrentSonarrEpisode; var sonarrEpisode = viewModel.CurrentSonarrEpisode;
foreach (var selectedSeriesSeason in SelectedSeries.Seasons){ foreach (var selectedSeriesSeason in SelectedSeries.Seasons){
foreach (var historyEpisode in selectedSeriesSeason.EpisodesList.Where(historyEpisode => historyEpisode.SonarrEpisodeId == sonarrEpisode.Id.ToString())){ foreach (var historyEpisode in selectedSeriesSeason.EpisodesList.Where(historyEpisode => historyEpisode.SonarrEpisodeId == sonarrEpisode.Id.ToString())){
historyEpisode.SonarrEpisodeId = string.Empty; historyEpisode.SonarrEpisodeId = string.Empty;
@ -162,7 +203,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
historyEpisode.SonarrIsMonitored = false; historyEpisode.SonarrIsMonitored = false;
} }
} }
episode.AssignSonarrEpisodeData(sonarrEpisode); episode.AssignSonarrEpisodeData(sonarrEpisode);
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
} }
@ -190,6 +231,26 @@ public partial class SeriesPageViewModel : ViewModelBase{
} }
} }
[RelayCommand]
public async Task DownloadEpisodeOnlyOptions(HistoryEpisode episode){
var downloadMode = SelectedDownloadMode;
if (downloadMode != EpisodeDownloadMode.Default){
await episode.DownloadEpisode(downloadMode);
}
}
[RelayCommand]
public async Task DownloadSeasonAllOnlyOptions(HistorySeason season){
var downloadMode = SelectedDownloadMode;
if (downloadMode != EpisodeDownloadMode.Default){
foreach (var episode in season.EpisodesList){
await episode.DownloadEpisode(downloadMode);
}
}
}
[RelayCommand] [RelayCommand]
public async Task DownloadSeasonMissingSonarr(HistorySeason season){ public async Task DownloadSeasonMissingSonarr(HistorySeason season){
foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){ foreach (var episode in season.EpisodesList.Where(episode => !episode.SonarrHasFile)){
@ -263,4 +324,14 @@ public partial class SeriesPageViewModel : ViewModelBase{
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}"); Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
} }
} }
partial void OnSelectedDownloadModeChanged(EpisodeDownloadMode value){
SelectedDownloadIcon = SelectedDownloadMode switch{
EpisodeDownloadMode.OnlyVideo => Symbol.Video,
EpisodeDownloadMode.OnlyAudio => Symbol.Audio,
EpisodeDownloadMode.OnlySubs => Symbol.ClosedCaption,
_ => Symbol.ClosedCaption
};
}
} }

View file

@ -0,0 +1,92 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Utils.Structs.History;
using CRD.Utils.UI;
using DynamicData;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogFeaturedMusicViewModel : ViewModelBase{
private readonly CustomContentDialog dialog;
[ObservableProperty]
private ObservableCollection<HistoryEpisode> _featuredMusicList = new();
[ObservableProperty]
private bool _musicInHistory;
private CrunchyMusicVideoList featuredMusic;
public ContentDialogFeaturedMusicViewModel(CustomContentDialog contentDialog, CrunchyMusicVideoList featuredMusic, bool crunOptionsHistoryIncludeCrArtists){
ArgumentNullException.ThrowIfNull(contentDialog);
this.featuredMusic = featuredMusic;
dialog = contentDialog;
dialog.Closed += DialogOnClosed;
dialog.PrimaryButtonClick += SaveButton;
if (crunOptionsHistoryIncludeCrArtists){
var episodeList = featuredMusic.Data
.Select(video =>
CrunchyrollManager.Instance.HistoryList
.FirstOrDefault(h => h.SeriesId == video.GetSeriesId())?
.Seasons.FirstOrDefault(s => s.SeasonId == video.GetSeasonId())?
.EpisodesList.FirstOrDefault(e => e.EpisodeId == video.GetEpisodeId()))
.Where(episode => episode != null)
.ToList();
if (episodeList.Count > 0){
FeaturedMusicList.Clear();
FeaturedMusicList!.AddRange(episodeList);
}
} else{
List<HistoryEpisode> episodeList =[];
foreach (var crunchyMusicVideo in featuredMusic.Data){
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = crunchyMusicVideo.GetEpisodeTitle(),
EpisodeDescription = crunchyMusicVideo.GetEpisodeDescription(),
EpisodeId = crunchyMusicVideo.GetEpisodeId(),
Episode = crunchyMusicVideo.GetEpisodeNumber(),
EpisodeSeasonNum = crunchyMusicVideo.GetSeasonNum(),
SpecialEpisode = crunchyMusicVideo.IsSpecialEpisode(),
HistoryEpisodeAvailableDubLang = crunchyMusicVideo.GetEpisodeAvailableDubLang(),
HistoryEpisodeAvailableSoftSubs = crunchyMusicVideo.GetEpisodeAvailableSoftSubs(),
EpisodeCrPremiumAirDate = crunchyMusicVideo.GetAvailableDate(),
EpisodeType = crunchyMusicVideo.GetEpisodeType(),
IsEpisodeAvailableOnStreamingService = true,
ThumbnailImageUrl = crunchyMusicVideo.GetImageUrl(),
};
episodeList.Add(newHistoryEpisode);
newHistoryEpisode.LoadImage();
}
if (episodeList.Count > 0){
FeaturedMusicList.Clear();
FeaturedMusicList.AddRange(episodeList);
}
}
}
[RelayCommand]
public void DownloadEpisode(HistoryEpisode episode){
episode.DownloadEpisode();
}
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
dialog.PrimaryButtonClick -= SaveButton;
}
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
dialog.Closed -= DialogOnClosed;
}
}

View file

@ -13,12 +13,13 @@
Unloaded="OnUnloaded" Unloaded="OnUnloaded"
Loaded="Control_OnLoaded"> Loaded="Control_OnLoaded">
<UserControl.Resources> <UserControl.Resources>
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" /> <ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" /> <ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
<ui:UiListToStringConverter x:Key="UiListToStringConverter" /> <ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" /> <ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" /> <ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter" />
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
</UserControl.Resources> </UserControl.Resources>
@ -406,6 +407,7 @@
<TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap" <TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
Text="{Binding SeriesDescription}" MinWidth="200"> Text="{Binding SeriesDescription}" MinWidth="200">
</TextBlock> </TextBlock>
<StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center" <StackPanel Grid.Row="3" Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}"> IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock> <TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="Available Dubs: "></TextBlock>
@ -419,7 +421,8 @@
FontSize="15" FontSize="15"
Opacity="0.8" Opacity="0.8"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
<StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center" <StackPanel Grid.Row="4" Orientation="Horizontal" VerticalAlignment="Center"
@ -435,7 +438,8 @@
FontSize="15" FontSize="15"
Opacity="0.8" Opacity="0.8"
TextWrapping="Wrap" /> TextWrapping="Wrap" />
</StackPanel> </StackPanel>
@ -516,7 +520,7 @@
<ToggleButton Margin="0 0 5 10" FontStyle="Italic" <ToggleButton Margin="0 0 5 10" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"
IsChecked="{Binding IsInactive}" IsChecked="{Binding IsInactive}"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleInactive}"> Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleInactive}">
<ToolTip.Tip> <ToolTip.Tip>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
@ -717,27 +721,41 @@
<Grid VerticalAlignment="Center"> <Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<StackPanel VerticalAlignment="Center"> <controls:SymbolIcon Grid.Column="0" IsVisible="{Binding !IsEpisodeAvailableOnStreamingService}"
<ui:EpisodeHighlightTextBlock Margin="0 0 5 0 "
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}" Symbol="AlertOn"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}" FontSize="18"
Episode="{Binding .}" HorizontalAlignment="Center"
StreamingService="Crunchyroll" VerticalAlignment="Center">
MinWidth="50" <ToolTip.Tip>
TextWrapping="NoWrap" <TextBlock Text="Episode unavailable — it might not be available on the streaming service or got moved to a different season" FontSize="15" />
TextTrimming="CharacterEllipsis"/> </ToolTip.Tip>
</controls:SymbolIcon>
<StackPanel Grid.Column="1" VerticalAlignment="Center">
<ui:EpisodeHighlightTextBlock
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
Season="{Binding $parent[controls:SettingsExpander].((history:HistorySeason)DataContext)}"
Episode="{Binding .}"
StreamingService="Crunchyroll"
MinWidth="50"
TextWrapping="NoWrap"
TextTrimming="CharacterEllipsis" />
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" <StackPanel Orientation="Horizontal" VerticalAlignment="Center"
IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}"> IsVisible="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
<TextBlock FontStyle="Italic" <TextBlock FontStyle="Italic"
FontSize="12" FontSize="12"
Opacity="0.8" Text="Dubs: "> Opacity="0.8" Text="Dubs: ">
</TextBlock> </TextBlock>
<ui:HighlightingTextBlock <ui:HighlightingTextBlock
Items="{Binding HistoryEpisodeAvailableDubLang}" Items="{Binding HistoryEpisodeAvailableDubLang}"
Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}" Series="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext)}"
@ -747,19 +765,20 @@
FontSize="12" FontSize="12"
Opacity="0.8" Opacity="0.8"
FontStyle="Italic" /> FontStyle="Italic" />
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="1" <StackPanel Grid.Column="2"
Orientation="Horizontal" Orientation="Horizontal"
VerticalAlignment="Center"> VerticalAlignment="Center">
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock> <TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center" <StackPanel VerticalAlignment="Center"
Margin="0 0 5 0" Margin="0 0 5 0"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}"> IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).SonarrSeriesId, Converter={StaticResource UiSonarrIdToVisibilityConverter}}">
<controls:ImageIcon <controls:ImageIcon
IsVisible="{Binding SonarrHasFile}" IsVisible="{Binding SonarrHasFile}"
Source="../Assets/sonarr.png" Source="../Assets/sonarr.png"
@ -806,8 +825,7 @@
<Button Margin="0 0 5 0" FontStyle="Italic" <Button Margin="0 0 5 0" FontStyle="Italic"
HorizontalAlignment="Right" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding DownloadEpisode}" Command="{Binding DownloadEpisodeDefault}">
CommandParameter="false">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" <controls:SymbolIcon Symbol="Download"
FontSize="18" /> FontSize="18" />
@ -817,13 +835,14 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right" <Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}" IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}" Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadEpisodeOnlyOptions}"
CommandParameter="true"> CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" /> <controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel> </StackPanel>
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" /> <TextBlock Text="Download Only" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Button> </Button>
@ -933,6 +952,69 @@
</StackPanel> </StackPanel>
</Button> </Button>
<StackPanel>
<ToggleButton x:Name="DropdownButtonOnlyDownload"
Margin="10 0 0 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonOnlyDownload, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonOnlyDownload}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel Orientation="Vertical">
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Content="Download All"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadSeasonAllOnlyOptions}"
CommandParameter="{Binding }">
</Button>
<Rectangle Height="1" Fill="Gray" Margin="0,8,0,8" />
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Video" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyVideo}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Audio" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyAudio}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Sub" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlySubs}" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic" <ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"

View file

@ -14,8 +14,10 @@
<UserControl.Resources> <UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" /> <ui:UiListToStringConverter x:Key="UiListToStringConverter" />
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" /> <ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
<ui:UiEnumToBoolConverter x:Key="EnumToBoolConverter" />
</UserControl.Resources> </UserControl.Resources>
<Grid> <Grid>
<Grid Margin="10"> <Grid Margin="10">
<Grid.RowDefinitions> <Grid.RowDefinitions>
@ -137,6 +139,19 @@
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<Button Command="{Binding UpdateData}" Margin="0 0 5 10">Refresh Series</Button> <Button Command="{Binding UpdateData}" Margin="0 0 5 10">Refresh Series</Button>
<Button Margin="0 0 5 10" FontStyle="Italic"
IsVisible="{Binding ShowFeaturedMusicButton}"
VerticalAlignment="Center"
Command="{Binding OpenFeaturedMusicDialog}">
<ToolTip.Tip>
<TextBlock Text="Featured Music" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Audio" FontSize="18" />
</StackPanel>
</Button>
<ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 5 10">Edit</ToggleButton> <ToggleButton IsChecked="{Binding EditMode}" Margin="0 0 5 10">Edit</ToggleButton>
<Button Margin="0 0 5 10" FontStyle="Italic" <Button Margin="0 0 5 10" FontStyle="Italic"
@ -324,7 +339,7 @@
</StackPanel> </StackPanel>
<StackPanel IsVisible="{Binding EditMode}"> <StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 5 0" <Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0" BorderThickness="0"
IsVisible="{Binding SonarrConnected}" IsVisible="{Binding SonarrConnected}"
Command="{Binding MatchSonarrSeries_Button}"> Command="{Binding MatchSonarrSeries_Button}">
@ -336,20 +351,17 @@
</Grid> </Grid>
</Button> </Button>
</StackPanel> </StackPanel>
<StackPanel IsVisible="{Binding EditMode}"> <StackPanel IsVisible="{Binding EditMode}">
<Button Height="30" Margin="0 0 10 0" <Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0" BorderThickness="0"
IsVisible="{Binding SonarrConnected}" IsVisible="{Binding SonarrConnected}"
Command="{Binding RefreshSonarrEpisodeMatch}"> Command="{Binding RefreshSonarrEpisodeMatch}">
<Grid> <Grid>
<StackPanel Orientation="Horizontal">
<controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" /> <controls:ImageIcon Source="../Assets/sonarr.png" Width="25" Height="25" />
<TextBlock Margin="5 0 0 0" Text="Rematch Episodes"></TextBlock>
</StackPanel>
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Rematch all Sonarr Episodes" FontSize="15" /> <TextBlock Text="Refresh all Sonarr Episode matches" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Grid> </Grid>
</Button> </Button>
@ -448,13 +460,12 @@
<TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock> <TextBlock Text="{Binding ReleaseDateFormated}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal" <StackPanel VerticalAlignment="Center" Margin="0 0 5 0" Orientation="Horizontal"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}"> IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">
<TextBlock Text="{Binding SonarrSeasonEpisodeText}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock> <TextBlock Text="{Binding SonarrSeasonEpisodeText}" VerticalAlignment="Center" FontSize="15" Opacity="0.8" Margin="0 0 20 0"></TextBlock>
<StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}" <StackPanel IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).ShowMonitoredBookmark}"
HorizontalAlignment="Center" HorizontalAlignment="Center"
@ -476,6 +487,7 @@
</controls:SymbolIcon> </controls:SymbolIcon>
</StackPanel> </StackPanel>
<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 $parent[UserControl].((vm:SeriesPageViewModel)DataContext).MatchSonarrEpisode_Button}" Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).MatchSonarrEpisode_Button}"
@ -488,7 +500,7 @@
<controls:ImageIcon IsVisible="{Binding !SonarrHasFile}" <controls:ImageIcon IsVisible="{Binding !SonarrHasFile}"
Source="../Assets/sonarr_inactive.png" Width="25" Source="../Assets/sonarr_inactive.png" Width="25"
Height="25" /> Height="25" />
</StackPanel> </StackPanel>
</Button> </Button>
@ -520,8 +532,7 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right" <Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding DownloadEpisode}" Command="{Binding DownloadEpisodeDefault}">
CommandParameter="false">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="18" /> <controls:SymbolIcon Symbol="Download" FontSize="18" />
</StackPanel> </StackPanel>
@ -533,13 +544,15 @@
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right" <Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center" VerticalAlignment="Center"
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}" IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
Command="{Binding DownloadEpisode}" Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).DownloadEpisodeOnlyOptions}"
CommandParameter="true"> CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel> </StackPanel>
<ToolTip.Tip> <ToolTip.Tip>
<TextBlock Text="Download Subs" FontSize="15" /> <TextBlock Text="Download Only" FontSize="15" />
</ToolTip.Tip> </ToolTip.Tip>
</Button> </Button>
@ -630,6 +643,70 @@
</StackPanel> </StackPanel>
</Button> </Button>
<StackPanel>
<ToggleButton x:Name="DropdownButtonOnlyDownload"
Margin="10 0 0 0" FontStyle="Italic" HorizontalAlignment="Right"
VerticalAlignment="Center">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadIcon}" FontSize="18" />
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonOnlyDownload, Mode=TwoWay}"
Placement="BottomEdgeAlignedRight"
PlacementTarget="{Binding ElementName=DropdownButtonOnlyDownload}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel Orientation="Vertical">
<Button Margin="10 5"
VerticalAlignment="Center"
HorizontalAlignment="Stretch"
Content="Download All"
Command="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).DownloadSeasonAllOnlyOptions}"
CommandParameter="{Binding }">
</Button>
<Rectangle Height="1" Fill="Gray" Margin="0,8,0,8" />
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Video" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyVideo}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Audio" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlyAudio}" VerticalAlignment="Center" />
</Grid>
<Grid Margin="8 0 5 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock Text="Sub" Grid.Column="0" VerticalAlignment="Center" />
<ToggleSwitch OffContent="" OnContent="" Grid.Column="1" IsChecked="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SelectedDownloadMode, Converter={StaticResource EnumToBoolConverter}, ConverterParameter=OnlySubs}" VerticalAlignment="Center" />
</Grid>
</StackPanel>
</Border>
</Popup>
</StackPanel>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel VerticalAlignment="Center" HorizontalAlignment="Center">
<ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic" <ToggleButton x:Name="SeasonOverride" Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center" VerticalAlignment="Center"

View file

@ -0,0 +1,99 @@
<ui:CustomContentDialog xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:CRD.ViewModels.Utils"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:CRD.Utils.UI"
x:DataType="vm:ContentDialogFeaturedMusicViewModel"
x:Class="CRD.Views.Utils.ContentDialogFeaturedMusicView">
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<ItemsControl ItemsSource="{Binding FeaturedMusicList}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Margin="10">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" x:Name="RootGrid">
<Image Width="320" Height="180" Stretch="Fill" HorizontalAlignment="Center" Source="/Assets/coming_soon_ep.jpg" />
<Image Width="320" Height="180" Source="{Binding ThumbnailImage}" Stretch="UniformToFill" />
<StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" IsVisible="{Binding $parent[UserControl].((vm:ContentDialogFeaturedMusicViewModel)DataContext).MusicInHistory}">
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Padding="5"
IsVisible="{Binding !WasDownloaded}">
<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"
Padding="5"
IsVisible="{Binding WasDownloaded}">
<Grid>
<Ellipse Width="25" Height="25" Fill="#21a556" />
<controls:SymbolIcon Symbol="Checkmark" FontSize="18" />
</Grid>
</Button>
</StackPanel>
<Border Grid.Row="0" Background="#80000000"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding #RootGrid.IsPointerOver}">
<Button FontStyle="Italic"
Background="Transparent"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:ContentDialogFeaturedMusicViewModel)DataContext).DownloadEpisode}"
CommandParameter="{Binding .}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Download" FontSize="24" />
</StackPanel>
</Button>
</Border>
</Grid>
<!-- 320 180 -->
<TextBlock Grid.Row="1" HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding EpisodeTitle}"
TextWrapping="Wrap"
Width="310"
FontSize="14"
Height="35"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding EpisodeTitle}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</Grid>
</ui:CustomContentDialog>

View file

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