Add - Added encoding option to muxing settings

Add - Added Custom encoding presets
Add - Added Skip Muxing to muxing settings
Add - Added Dubs to file name settings
Add - IP check in settings to check if VPN is being used
Add - Dubs to "Add Downloads" Tab
Add - Series folder link to history series if it finds the folder
Add - Added command line arguments
Add - Added proxy settings to the Settings tab (changes require a restart to take effect)
Add - Added option to set "Sign" subs forced flag
Add - Added option to set "CC" subs "hearing-impaired" flag
Add - Added encoding presets editing
Add - Added CC subtitles font option to the settings
Add - Added available dubs to history episodes

Chg - Defaults to system accent color when no color is selected in the settings
Chg - Audio only mux to only copy and not encode
Chg - Update dialog
Chg - Light mode color adjustments
Chg - Http Connection change to detect proxy (Clash)
Chg - Settings filename description
Chg - Changed FPS on encoding presets to 24fps
Chg - Adjusted encoding to allow h264_nvenc & hevc_nvenc
Chg - Moved sync timing folders from the Windows temp folder to the application root's temp folder
Chg - The temp folder will now be deleted automatically when empty

Fix - Locale not correctly applied to Urls in the "Add Downloads" Tab
Fix - Locale not correctly applied to Search in the "Add Downloads" Tab
Fix - Scrolling issue in settings
Fix - Fix crash when removing streaming tokens (TOO_MANY_ACTIVE_STREAMS)
Fix - Search didn't reset correctly
Fix - Clash proxy didn't work
Fix - Chapters were always taken from the original version (mainly JP)
Fix - Connection issue
Fix - Fixed an issue where proxy settings were only available when history was enabled
Fix - Fixed scrolling issues with certain series in the "Add Downloads" tab
Fix - Fixed an issue where History Series appeared incomplete after being added then deleted and re-added
Fix - Fixed a crash related to sync timing
This commit is contained in:
Elwador 2024-09-30 20:08:37 +02:00
parent b5e894ba82
commit 5d94025fcc
41 changed files with 1749 additions and 317 deletions

View file

@ -1,8 +1,14 @@
using System;
using System.IO;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using CRD.ViewModels;
using MainWindow = CRD.Views.MainWindow;
using System.Linq;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
namespace CRD;
@ -13,11 +19,22 @@ public partial class App : Application{
public override void OnFrameworkInitializationCompleted(){
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop){
desktop.MainWindow = new MainWindow{
DataContext = new MainWindowViewModel(),
};
var isHeadless = Environment.GetCommandLineArgs().Contains("--headless");
var manager = ProgramManager.Instance;
if (!isHeadless){
desktop.MainWindow = new MainWindow{
DataContext = new MainWindowViewModel(manager),
};
}
}
base.OnFrameworkInitializationCompleted();
}
}

View file

@ -123,7 +123,7 @@ public class CrEpisode(){
if (!serieshasversions){
Console.WriteLine("Couldn\'t find versions on episode, fell back to old method.");
Console.WriteLine("Couldn\'t find versions on episode, added languages with language array.");
}
return episode;

View file

@ -116,12 +116,12 @@ public class CrSeries(){
}
public async Task<CrunchySeriesList?> ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data){
public async Task<CrunchySeriesList?> ListSeriesId(string id, string crLocale, CrunchyMultiDownload? data, bool forcedLocale = false){
await crunInstance.CrAuth.RefreshToken(true);
bool serieshasversions = true;
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale); // one piece - GRMG8ZQZR
CrSeriesSearch? parsedSeries = await ParseSeriesById(id, crLocale,forcedLocale);
if (parsedSeries == null || parsedSeries.Data == null){
Console.Error.WriteLine("Parse Data Invalid");
@ -142,7 +142,7 @@ public class CrSeries(){
if (data?.S != null && s.Id != data.Value.S) continue;
int fallbackIndex = 0;
if (cachedSeasonID != s.Id){
seasonData = await GetSeasonDataById(s.Id, "");
seasonData = await GetSeasonDataById(s.Id, forcedLocale ? crLocale : "");
cachedSeasonID = s.Id;
}
@ -261,7 +261,7 @@ public class CrSeries(){
}
if (!serieshasversions){
Console.WriteLine("Couldn\'t find versions on some episodes, fell back to old method.");
Console.WriteLine("Couldn\'t find versions on some episodes, added languages with language array.");
}
CrunchySeriesList crunchySeriesList = new CrunchySeriesList();
@ -272,9 +272,12 @@ public class CrSeries(){
var value = kvp.Value;
var images = (value.Items[0].Images?.Thumbnail ?? new List<List<Image>>{ new List<Image>{ new Image{ Source = "/notFound.png" } } });
var seconds = (int)Math.Floor(value.Items[0].DurationMs / 1000.0);
var langList = value.Langs.Select(a => a.CrLocale).ToList();
Languages.SortListByLangList(langList);
return new Episode{
E = key.StartsWith("E") ? key.Substring(1) : key,
Lang = value.Langs.Select(a => a.Code).ToList(),
Lang = langList,
Name = value.Items[0].Title,
Season = Helpers.ExtractNumberAfterS(value.Items[0].Identifier) ?? value.Items[0].SeasonNumber.ToString(),
SeriesTitle = Regex.Replace(value.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd(),
@ -439,12 +442,15 @@ public class CrSeries(){
}
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale){
public async Task<CrSearchSeriesBase?> Search(string searchString, string? crLocale, bool forced = false){
await crunInstance.CrAuth.RefreshToken(true);
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
if (forced){
query["force_locale"] = crLocale;
}
}
query["q"] = searchString;

View file

@ -13,6 +13,7 @@ using System.Xml;
using Avalonia.Media;
using CRD.Utils;
using CRD.Utils.DRM;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Files;
using CRD.Utils.HLS;
using CRD.Utils.Muxing;
@ -99,18 +100,19 @@ public class CrunchyrollManager{
options.FileName = "${seriesTitle} - S${season}E${episode} [${height}p]";
options.Partsize = 10;
options.DlSubs = new List<string>{ "en-US" };
options.Skipmux = false;
options.SkipMuxing = false;
options.MkvmergeOptions = new List<string>{ "--no-date", "--disable-track-statistics-tags", "--engage no_variable_data" };
options.FfmpegOptions = new();
options.DefaultAudio = "ja-JP";
options.DefaultSub = "en-US";
options.CcTag = "CC";
options.CcSubsFont = "Trebuchet MS";
options.FsRetryTime = 5;
options.Numbers = 2;
options.Timeout = 15000;
options.DubLang = new List<string>(){ "ja-JP" };
options.SimultaneousDownloads = 2;
options.AccentColor = Colors.SlateBlue.ToString();
// options.AccentColor = Colors.SlateBlue.ToString();
options.Theme = "System";
options.SelectedCalendarLanguage = "en-us";
options.CalendarDubFilter = "none";
@ -145,7 +147,7 @@ public class CrunchyrollManager{
HasPremium = false,
};
Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
Console.WriteLine($"CDM available: {_widevine.canDecrypt}");
}
public async Task Init(){
@ -157,7 +159,7 @@ public class CrunchyrollManager{
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
CrAuth.LoginWithToken();
await CrAuth.LoginWithToken();
} else{
await CrAuth.AuthAnonymous();
}
@ -181,6 +183,26 @@ public class CrunchyrollManager{
await SonarrClient.Instance.RefreshSonarr();
}
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[];
foreach (var file in jsonFiles){
try{
// Read the content of the JSON file
var jsonContent = File.ReadAllText(file);
// Deserialize the JSON content into a MyClass object
var obj = Helpers.Deserialize<VideoPreset>(jsonContent, null);
if (obj != null){
FfmpegEncoding.AddPreset(obj);
} else{
Console.Error.WriteLine("Failed to add Preset to Available Presets List");
}
} catch (Exception ex){
Console.Error.WriteLine($"Failed to deserialize file {file}: {ex.Message}");
}
}
}
@ -212,7 +234,7 @@ public class CrunchyrollManager{
return false;
}
if (options.Skipmux == false){
if (options.SkipMuxing == false){
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
@ -257,6 +279,21 @@ public class CrunchyrollManager{
foreach (var merger in mergers){
merger.CleanUp();
if (CrunOptions.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Encoding"
};
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName));
}
if (CrunOptions.DownloadToTempFolder){
await MoveFromTempFolder(merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
@ -286,6 +323,20 @@ public class CrunchyrollManager{
result.merger.CleanUp();
}
if (CrunOptions.IsEncodeEnabled){
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Encoding"
};
QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName));
}
if (CrunOptions.DownloadToTempFolder){
await MoveFromTempFolder(result.merger, data, res.TempFolderPath, res.Data.Where(e => e.Type == DownloadMediaType.Subtitle));
}
@ -306,8 +357,33 @@ public class CrunchyrollManager{
}
} else{
Console.WriteLine("Skipping mux");
res.Data.ForEach(file => Helpers.DeleteFile(file.Path + ".resume"));
if (CrunOptions.DownloadToTempFolder){
if (string.IsNullOrEmpty(res.TempFolderPath) || !Directory.Exists(res.TempFolderPath)){
Console.WriteLine("Invalid or non-existent temp folder path.");
} else{
// Move files
foreach (var downloadedMedia in res.Data){
await MoveFile(downloadedMedia.Path ?? string.Empty, res.TempFolderPath, data.DownloadPath ?? CfgManager.PathVIDEOS_DIR);
}
}
}
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Done = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Done - Skipped muxing"
};
if (CrunOptions.RemoveFinishedDownload){
QueueManager.Instance.Queue.Remove(data);
}
}
QueueManager.Instance.ActiveDownloads--;
QueueManager.Instance.Queue.Refresh();
@ -668,7 +744,12 @@ public class CrunchyrollManager{
List<string> compiledChapters = new List<string>();
if (options.Chapters){
await ParseChapters(primaryVersion.Guid ?? mediaGuid, compiledChapters);
await ParseChapters(mediaGuid, compiledChapters);
if (compiledChapters.Count == 0 && primaryVersion.MediaGuid != null && mediaGuid != primaryVersion.MediaGuid){
Console.Error.WriteLine("Chapters empty trying to get original version chapters - might not match with video");
await ParseChapters(primaryVersion.MediaGuid, compiledChapters);
}
}
#endregion
@ -703,7 +784,7 @@ public class CrunchyrollManager{
var pbData = fetchPlaybackData.pbData;
List<string> hsLangs = new List<string>();
var pbStreams = pbData.Data?[0];
var pbStreams = pbData.Data;
var streams = new List<StreamDetailsPop>();
variables.Add(new Variable("title", data.EpisodeTitle ?? string.Empty, true));
@ -712,34 +793,30 @@ public class CrunchyrollManager{
variables.Add(new Variable("seriesTitle", data.SeriesTitle ?? string.Empty, true));
variables.Add(new Variable("seasonTitle", data.SeasonTitle ?? string.Empty, true));
variables.Add(new Variable("season", !string.IsNullOrEmpty(data.Season) ? Math.Round(double.Parse(data.Season, CultureInfo.InvariantCulture), 1) : 0, false));
variables.Add(new Variable("dubs", string.Join(", ", data.SelectedDubs ??[]), true));
if (pbStreams?.Keys != null){
foreach (var key in pbStreams.Keys){
if ((key.Contains("hls") || key.Contains("dash")) &&
!(key.Contains("hls") && key.Contains("drm")) &&
!((!_widevine.canDecrypt || !File.Exists(CfgManager.PathMP4Decrypt)) && key.Contains("drm")) &&
!key.Contains("trailer")){
var pb = pbStreams[key].Select(v => {
v.Value.HardsubLang = v.Value.HardsubLocale != null
? Languages.FixAndFindCrLc(v.Value.HardsubLocale.GetEnumMemberValue()).Locale
: null;
if (v.Value.HardsubLocale != null && v.Value.HardsubLang != null && !hsLangs.Contains(v.Value.HardsubLocale.GetEnumMemberValue())){
hsLangs.Add(v.Value.HardsubLang);
}
return new StreamDetailsPop{
Url = v.Value.Url,
HardsubLocale = v.Value.HardsubLocale,
HardsubLang = v.Value.HardsubLang,
AudioLang = v.Value.AudioLang,
Type = v.Value.Type,
Format = key,
};
}).ToList();
streams.AddRange(pb);
var pb = pbStreams.Select(v => {
v.Value.HardsubLang = v.Value.HardsubLocale != null
? Languages.FixAndFindCrLc(v.Value.HardsubLocale.GetEnumMemberValue()).Locale
: null;
if (v.Value.HardsubLocale != null && v.Value.HardsubLang != null && !hsLangs.Contains(v.Value.HardsubLocale.GetEnumMemberValue())){
hsLangs.Add(v.Value.HardsubLang);
}
}
return new StreamDetailsPop{
Url = v.Value.Url,
HardsubLocale = v.Value.HardsubLocale,
HardsubLang = v.Value.HardsubLang,
AudioLang = v.Value.AudioLang,
Type = v.Value.Type,
Format = "drm_adaptive_dash",
};
}).ToList();
streams.AddRange(pb);
if (streams.Count < 1){
Console.WriteLine("No full streams found!");
@ -773,19 +850,29 @@ public class CrunchyrollManager{
Console.WriteLine($"Selecting stream with {Languages.Locale2language(options.Hslang).Language} hardsubs");
streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == options.Hslang).ToList();
} else{
Console.Error.WriteLine($"Selected stream with {Languages.Locale2language(options.Hslang).Language} hardsubs not available");
Console.Error.WriteLine($"Selected stream with {Languages.Locale2language(options.Hslang).CrLocale} hardsubs not available");
if (hsLangs.Count > 0){
Console.Error.WriteLine("Try hardsubs stream: " + string.Join(", ", hsLangs));
}
dlFailed = true;
if (dlVideoOnce && CrunOptions.DlVideoOnce){
streams = streams.Where((s) => {
if (s.HardsubLang != "-"){
return false;
}
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "Hardsubs not available"
};
return true;
}).ToList();
} else{
dlFailed = true;
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "Hardsubs not available"
};
}
}
} else{
streams = streams.Where((s) => {
@ -805,7 +892,7 @@ public class CrunchyrollManager{
dlFailed = true;
}
Console.WriteLine("Selecting raw stream");
Console.WriteLine("Selecting stream");
}
StreamDetailsPop? curStream = null;
@ -816,7 +903,7 @@ public class CrunchyrollManager{
for (int i = 0; i < streams.Count; i++){
string isSelected = options.Kstream == i + 1 ? "+" : " ";
Console.WriteLine($"Full stream found! ({isSelected}{i + 1}: {streams[i].Type})");
Console.WriteLine($"Full stream: ({isSelected}{i + 1}: {streams[i].Type})");
}
Console.WriteLine("Downloading video...");
@ -983,7 +1070,7 @@ public class CrunchyrollManager{
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
.ToArray());
@ -1288,7 +1375,7 @@ public class CrunchyrollManager{
try{
// Parsing and constructing the file names
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.Name), variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale), variables, options.Numbers, options.Override).ToArray());
if (Path.IsPathRooted(outFile)){
tsFile = outFile;
} else{
@ -1418,8 +1505,8 @@ public class CrunchyrollManager{
private static async Task DownloadSubtitles(CrDownloadOptions options, PlaybackData pbData, string audDub, string fileName, List<DownloadedMedia> files, string fileDir, CrunchyEpMeta data, bool needsDelay,
DownloadedMedia videoDownloadMedia){
if (pbData.Meta != null && (pbData.Meta.Subtitles is{ Count: > 0 } || pbData.Meta.Captions is{ Count: > 0 })){
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ?? [];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ?? [];
List<SubtitleInfo> subsData = pbData.Meta.Subtitles?.Values.ToList() ??[];
List<Caption> capsData = pbData.Meta.Captions?.Values.ToList() ??[];
var subsDataMapped = subsData.Select(s => {
var subLang = Languages.FixAndFindCrLc((s.Locale ?? Locale.DefaulT).GetEnumMemberValue());
return new{
@ -1507,7 +1594,7 @@ public class CrunchyrollManager{
assBuilder.AppendLine("[V4+ Styles]");
assBuilder.AppendLine("Format: Name, Fontname, Fontsize, PrimaryColour, SecondaryColour, OutlineColour, BackColour, Bold, Italic, "
+ "Underline, StrikeOut, ScaleX, ScaleY, Spacing, Angle, BorderStyle, Outline, Shadow, Alignment, MarginL, MarginR, MarginV, Encoding");
assBuilder.AppendLine("Style: Default,Trebuchet MS,24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0018,1");
assBuilder.AppendLine($"Style: Default,{options.CcSubsFont ?? "Trebuchet MS"},24,&H00FFFFFF,&H000000FF,&H00000000,&H00000000,0,0,0,0,100,100,0,0,1,2,1,2,0010,0010,0018,1");
assBuilder.AppendLine();
assBuilder.AppendLine("[Events]");
assBuilder.AppendLine("Format: Layer, Start, End, Style, Name, MarginL, MarginR, MarginV, Effect, Text");
@ -1665,7 +1752,7 @@ public class CrunchyrollManager{
private async Task<(bool IsOk, PlaybackData pbData, string error)> FetchPlaybackData(string mediaId, string mediaGuidId, bool music){
var temppbData = new PlaybackData{
Total = 0,
Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>()
Data = new Dictionary<string, StreamDetails>()
};
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{CrunOptions.StreamEndpoint}/play";
@ -1719,7 +1806,7 @@ public class CrunchyrollManager{
private async Task<PlaybackData> ProcessPlaybackResponseAsync(string responseContent, string mediaId, string mediaGuidId){
var temppbData = new PlaybackData{
Total = 0,
Data = new List<Dictionary<string, Dictionary<string, StreamDetails>>>()
Data = new Dictionary<string, StreamDetails>()
};
var playStream = Helpers.Deserialize<CrunchyStreamData>(responseContent, SettingsJsonSerializerSettings);
@ -1745,9 +1832,7 @@ public class CrunchyrollManager{
HardsubLocale = Locale.DefaulT
};
temppbData.Data.Add(new Dictionary<string, Dictionary<string, StreamDetails>>{
{ "drm_adaptive_dash", derivedPlayCrunchyStreams }
});
temppbData.Data = derivedPlayCrunchyStreams;
temppbData.Total = 1;
temppbData.Meta = new PlaybackMeta{
@ -1903,7 +1988,7 @@ public class CrunchyrollManager{
return true;
}
Console.WriteLine("Old Chapter API request failed");
Console.Error.WriteLine("Old Chapter API request failed");
return false;
}

View file

@ -22,10 +22,10 @@ public class History(){
public async Task CRUpdateSeries(string seriesId, string? seasonId){
await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "en-US", true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
if (parsedSeries == null){
Console.Error.WriteLine("Parse Data Invalid");
Console.Error.WriteLine("Parse Data Invalid - series is maybe only available with VPN or got deleted");
return;
}
@ -242,17 +242,37 @@ public class History(){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
if (historyEpisode == null){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
EpisodeId = crunchyEpisode.Id,
Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "",
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList),
HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales),
};
historySeason.EpisodesList.Add(newHistoryEpisode);
} else{
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
//Update existing episode
historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode);
historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _);
@ -260,6 +280,9 @@ public class History(){
historyEpisode.EpisodeId = crunchyEpisode.Id;
historyEpisode.Episode = crunchyEpisode.Episode;
historyEpisode.EpisodeSeasonNum = Helpers.ExtractNumberAfterS(crunchyEpisode.Identifier) ?? crunchyEpisode.SeasonNumber + "";
historyEpisode.HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList);
historyEpisode.HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales);
}
}
@ -335,6 +358,14 @@ public class History(){
if (cachedSeries == null || (cachedSeries.Data != null && cachedSeries.Data.First().Id != seriesId)){
cachedSeries = await crunInstance.CrSeries.SeriesById(seriesId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
} else{
if (cachedSeries?.Data != null){
var series = cachedSeries.Data.First();
historySeries.SeriesDescription = series.Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries);
historySeries.SeriesTitle = series.Title;
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
}
return;
}
@ -343,8 +374,8 @@ public class History(){
historySeries.SeriesDescription = series.Description;
historySeries.ThumbnailImageUrl = GetSeriesThumbnail(cachedSeries);
historySeries.SeriesTitle = series.Title;
historySeries.HistorySeriesAvailableDubLang = series.AudioLocales;
historySeries.HistorySeriesAvailableSoftSubs = series.SubtitleLocales;
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
}
}
@ -474,6 +505,16 @@ public class History(){
};
foreach (var crunchyEpisode in seasonData){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
Languages.SortListByLangList(langList);
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
@ -481,6 +522,8 @@ public class History(){
Episode = crunchyEpisode.Episode,
EpisodeSeasonNum = Helpers.ExtractNumberAfterS(firstEpisode.Identifier) ?? firstEpisode.SeasonNumber + "",
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = langList,
HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales,
};
newSeason.EpisodesList.Add(newHistoryEpisode);

View file

@ -0,0 +1,176 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
using FluentAvalonia.Styling;
namespace CRD.Downloader;
public partial class ProgramManager : ObservableObject{
#region Singelton
private static ProgramManager? _instance;
private static readonly object Padlock = new();
public static ProgramManager Instance{
get{
if (_instance == null){
lock (Padlock){
if (_instance == null){
_instance = new ProgramManager();
}
}
}
return _instance;
}
}
#endregion
#region Observables
[ObservableProperty]
private bool _fetchingData;
[ObservableProperty]
private bool _updateAvailable = true;
[ObservableProperty]
private bool _finishedLoading = false;
#endregion
private readonly FluentAvaloniaTheme? _faTheme;
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
private bool exitOnTaskFinish = false;
public ProgramManager(){
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
foreach (var arg in Environment.GetCommandLineArgs()){
if (arg == "--historyRefreshAll"){
taskQueue.Enqueue(RefreshAll);
} else if (arg == "--historyAddToQueue"){
taskQueue.Enqueue(AddMissingToQueue);
} else if (arg == "--exit"){
exitOnTaskFinish = true;
}
}
Init();
CleanUpOldUpdater();
}
private async Task RefreshAll(){
FetchingData = true;
foreach (var item in CrunchyrollManager.Instance.HistoryList){
item.SetFetchingData();
}
for (int i = 0; i < CrunchyrollManager.Instance.HistoryList.Count; i++){
await CrunchyrollManager.Instance.HistoryList[i].FetchData("");
CrunchyrollManager.Instance.HistoryList[i].UpdateNewEpisodes();
}
FetchingData = false;
CrunchyrollManager.Instance.History.SortItems();
}
private async Task AddMissingToQueue(){
var tasks = CrunchyrollManager.Instance.HistoryList
.Select(item => item.AddNewMissingToDownloads());
await Task.WhenAll(tasks);
while (QueueManager.Instance.Queue.Any(e => e.DownloadProgress != null && e.DownloadProgress.Done != true)){
Console.WriteLine("Waiting for downloads to complete...");
await Task.Delay(2000); // Wait for 2 second before checking again
}
}
private async void Init(){
CrunchyrollManager.Instance.InitOptions();
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
}
if (_faTheme != null && Application.Current != null){
if (CrunchyrollManager.Instance.CrunOptions.Theme == "System"){
_faTheme.PreferSystemTheme = true;
} else if (CrunchyrollManager.Instance.CrunOptions.Theme == "Dark"){
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
} else{
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
}
await CrunchyrollManager.Instance.Init();
FinishedLoading = true;
await WorkOffArgsTasks();
}
private async Task WorkOffArgsTasks(){
if (taskQueue.Count == 0){
return;
}
while (taskQueue.Count > 0){
var task = taskQueue.Dequeue();
await task(); // Execute the task asynchronously
}
Console.WriteLine("All tasks are completed.");
if (exitOnTaskFinish){
Console.WriteLine("Exiting...");
IClassicDesktopStyleApplicationLifetime? lifetime = (IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime;
if (lifetime != null){
lifetime.Shutdown();
} else{
Environment.Exit(0);
}
}
}
private void CleanUpOldUpdater(){
string backupFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Updater.exe.bak");
if (File.Exists(backupFilePath)){
try{
File.Delete(backupFilePath);
Console.WriteLine($"Deleted old updater file: {backupFilePath}");
} catch (Exception ex){
Console.Error.WriteLine($"Failed to delete old updater file: {ex.Message}");
}
} else{
Console.WriteLine("No old updater file found to delete.");
}
}
}

View file

@ -1,5 +1,6 @@
using System;
using Avalonia;
using System.Linq;
namespace CRD;
@ -8,13 +9,29 @@ sealed class Program{
// SynchronizationContext-reliant code before AppMain is called: things aren't initialized
// yet and stuff might break.
[STAThread]
public static void Main(string[] args) => BuildAvaloniaApp()
.StartWithClassicDesktopLifetime(args);
public static void Main(string[] args){
var isHeadless = args.Contains("--headless");
BuildAvaloniaApp(isHeadless).StartWithClassicDesktopLifetime(args);
}
// Avalonia configuration, don't remove; also used by visual designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
// public static AppBuilder BuildAvaloniaApp()
// => AppBuilder.Configure<App>()
// .UsePlatformDetect()
// .WithInterFont()
// .LogToTrace();
public static AppBuilder BuildAvaloniaApp(bool isHeadless){
var builder = AppBuilder.Configure<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace();
if (isHeadless){
Console.WriteLine("Running in headless mode...");
}
return builder;
}
}

View file

@ -73,7 +73,10 @@ public class Widevine{
}
public async Task<List<ContentKey>> getKeys(string? pssh, string licenseServer, Dictionary<string, string> authData){
if (pssh == null || !canDecrypt) return new List<ContentKey>();
if (pssh == null || !canDecrypt){
Console.Error.WriteLine("Missing pssh or cdm files");
return new List<ContentKey>();
}
try{
byte[] psshBuffer = Convert.FromBase64String(pssh);

View file

@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CRD.Utils.Ffmpeg_Encoding;
public class FfmpegEncoding{
public static readonly List<VideoPreset> presets = new List<VideoPreset>{
// AV1 Software
new(){PresetName = "AV1 1080p24",Codec = "libaom-av1", Resolution = "1920:1080", FrameRate = "24", Crf = 30 } ,
new(){PresetName = "AV1 720p24", Codec = "libaom-av1", Resolution = "1280:720", FrameRate = "24", Crf = 30 } ,
new(){PresetName = "AV1 480p24", Codec = "libaom-av1", Resolution = "854:480", FrameRate = "24", Crf = 30 } ,
new(){PresetName = "AV1 360p24", Codec = "libaom-av1", Resolution = "640:360", FrameRate = "24", Crf = 30 } ,
new(){PresetName = "AV1 240p24", Codec = "libaom-av1", Resolution = "426:240", FrameRate = "24", Crf = 30 } ,
// H.265 Software
new(){PresetName = "H.265 1080p24", Codec = "libx265", Resolution = "1920:1080", FrameRate = "24", Crf = 28 } ,
new(){PresetName = "H.265 720p24", Codec = "libx265", Resolution = "1280:720", FrameRate = "24", Crf = 28 } ,
new(){PresetName = "H.265 480p24", Codec = "libx265", Resolution = "854:480", FrameRate = "24", Crf = 28 } ,
new(){PresetName = "H.265 360p24", Codec = "libx265", Resolution = "640:360", FrameRate = "24", Crf = 28 } ,
new(){PresetName = "H.265 240p24", Codec = "libx265", Resolution = "426:240", FrameRate = "24", Crf = 28 } ,
// H.264 Software
new(){ PresetName = "H.264 1080p24",Codec = "libx264", Resolution = "1920:1080", FrameRate = "24", Crf = 23 } ,
new(){PresetName = "H.264 720p24", Codec = "libx264", Resolution = "1280:720", FrameRate = "24", Crf = 23 } ,
new(){PresetName = "H.264 480p24", Codec = "libx264", Resolution = "854:480", FrameRate = "24", Crf = 23 },
new(){PresetName = "H.264 360p24", Codec = "libx264", Resolution = "640:360", FrameRate = "24", Crf = 23 } ,
new(){PresetName = "H.264 240p24", Codec = "libx264", Resolution = "426:240", FrameRate = "24", Crf = 23 } ,
};
public static VideoPreset? GetPreset(string presetName){
var preset = presets.FirstOrDefault(x => x.PresetName == presetName);
if (preset != null){
return preset;
}
Console.Error.WriteLine($"Preset {presetName} not found.");
return null;
}
public static void AddPreset(VideoPreset preset){
if (presets.Exists(x => x.PresetName == preset.PresetName)){
Console.Error.WriteLine($"Preset {preset.PresetName} already exists.");
return;
}
presets.Add(preset);
}
}
public class VideoPreset{
public string? PresetName{ get; set; }
public string? Codec{ get; set; }
public string? Resolution{ get; set; }
public string? FrameRate{ get; set; }
public int Crf{ get; set; }
public List<string> AdditionalParameters { get; set; } = new List<string>();
}

View file

@ -31,6 +31,7 @@ public class CfgManager{
public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine");
public static readonly string PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video");
public static readonly string PathENCODING_PRESETS_DIR = Path.Combine(WorkingDirectory, "presets");
public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp");
public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "video");
@ -215,12 +216,12 @@ public class CfgManager{
return;
}
WriteJsonToFile(PathCrHistory, CrunchyrollManager.Instance.HistoryList);
WriteJsonToFileCompressed(PathCrHistory, CrunchyrollManager.Instance.HistoryList);
}
private static object fileLock = new object();
public static void WriteJsonToFile(string pathToFile, object obj){
public static void WriteJsonToFileCompressed(string pathToFile, object obj){
try{
// Check if the directory exists; if not, create it.
string directoryPath = Path.GetDirectoryName(pathToFile);
@ -241,6 +242,27 @@ public class CfgManager{
Console.Error.WriteLine($"An error occurred: {ex.Message}");
}
}
public static void WriteJsonToFile(string pathToFile, object obj){
try{
// Check if the directory exists; if not, create it.
string directoryPath = Path.GetDirectoryName(pathToFile);
if (!Directory.Exists(directoryPath)){
Directory.CreateDirectory(directoryPath);
}
lock (fileLock){
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
using (var streamWriter = new StreamWriter(fileStream))
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
var serializer = new JsonSerializer();
serializer.Serialize(jsonWriter, obj);
}
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
}
}
public static string DecompressJsonFile(string pathToFile){
try{

View file

@ -23,6 +23,7 @@ public class FileNameManager{
if (variable == null){
Console.Error.WriteLine($"[ERROR] Found variable '{match}' in fileName but no values was internally found!");
input = input.Replace(match, "");
continue;
}
@ -110,16 +111,16 @@ public class FileNameManager{
}
public static void DeleteEmptyFolders(string rootFolderPath){
public static void DeleteEmptyFolders(string rootFolderPath, bool deleteRootIfEmpty = true){
if (string.IsNullOrEmpty(rootFolderPath) || !Directory.Exists(rootFolderPath)){
Console.WriteLine("Invalid directory path.");
return;
}
DeleteEmptyFoldersRecursive(rootFolderPath, isRoot: true);
DeleteEmptyFoldersRecursive(rootFolderPath, isRoot: true, deleteRootIfEmpty);
}
private static bool DeleteEmptyFoldersRecursive(string folderPath, bool isRoot = false){
private static bool DeleteEmptyFoldersRecursive(string folderPath, bool isRoot = false, bool deleteRootIfEmpty = true){
bool isFolderEmpty = true;
try{
@ -137,6 +138,12 @@ public class FileNameManager{
return true;
}
if (isRoot && deleteRootIfEmpty && isFolderEmpty && Directory.GetFiles(folderPath).Length == 0){
Directory.Delete(folderPath);
Console.WriteLine($"Deleted empty root folder: {folderPath}");
return true;
}
return false;
} catch (Exception ex){
Console.WriteLine($"An error occurred while deleting folder {folderPath}: {ex.Message}");

View file

@ -8,8 +8,10 @@ using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.JsonConv;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
@ -288,6 +290,74 @@ public class Helpers{
}
}
public static async Task<(bool IsOk, int ErrorCode)> RunFFmpegWithPresetAsync(string inputFilePath, VideoPreset preset){
try{
string outputExtension = Path.GetExtension(inputFilePath);
string directory = Path.GetDirectoryName(inputFilePath);
string fileNameWithoutExtension = Path.GetFileNameWithoutExtension(inputFilePath);
string tempOutputFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}_output{outputExtension}");
string additionalParams = string.Join(" ", preset.AdditionalParameters);
string qualityOption;
if (preset.Codec == "h264_nvenc" || preset.Codec == "hevc_nvenc"){
qualityOption = $"-cq {preset.Crf}"; // For NVENC
} else if (preset.Codec == "h264_qsv" || preset.Codec == "hevc_qsv"){
qualityOption = $"-global_quality {preset.Crf}"; // For Intel QSV
} else if (preset.Codec == "h264_amf" || preset.Codec == "hevc_amf"){
qualityOption = $"-qp {preset.Crf}"; // For AMD VCE
} else{
qualityOption = $"-crf {preset.Crf}"; // For software codecs like libx264/libx265
}
string ffmpegCommand = $"-loglevel warning -i \"{inputFilePath}\" -c:v {preset.Codec} {qualityOption} -vf \"scale={preset.Resolution},fps={preset.FrameRate}\" {additionalParams} \"{tempOutputFilePath}\"";
using (var process = new Process()){
process.StartInfo.FileName = CfgManager.PathFFMPEG;
process.StartInfo.Arguments = ffmpegCommand;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.OutputDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.Error.WriteLine($"{e.Data}");
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
bool isSuccess = process.ExitCode == 0;
if (isSuccess){
// Delete the original input file
File.Delete(inputFilePath);
// Rename the output file to the original name
File.Move(tempOutputFilePath, inputFilePath);
} else{
// If something went wrong, delete the temporary output file
File.Delete(tempOutputFilePath);
Console.Error.WriteLine("FFmpeg processing failed.");
}
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
return (IsOk: false, ErrorCode: -1);
}
}
public static double CalculateCosineSimilarity(string text1, string text2){
var vector1 = ComputeWordFrequency(text1);
var vector2 = ComputeWordFrequency(text2);
@ -368,13 +438,25 @@ public class Helpers{
}
public static async Task<Bitmap?> LoadImage(string imageUrl){
public static async Task<Bitmap?> LoadImage(string imageUrl,int desiredWidth = 0,int desiredHeight = 0){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
return new Bitmap(stream);
var bitmap = new Bitmap(stream);
if (desiredWidth != 0 && desiredHeight != 0){
var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(desiredWidth, desiredHeight));
bitmap.Dispose();
return scaledBitmap;
}
return bitmap;
}
}
} catch (Exception ex){

View file

@ -38,10 +38,42 @@ public class HttpClientReq{
private HttpClient client;
private Dictionary<string, CookieCollection> cookieStore;
private HttpClientHandler handler;
public HttpClientReq(){
cookieStore = new Dictionary<string, CookieCollection>();
client = new HttpClient(CreateHttpClientHandler());
IWebProxy systemProxy = WebRequest.DefaultWebProxy;
HttpClientHandler handler = new HttpClientHandler();
if (CrunchyrollManager.Instance.CrunOptions.ProxyEnabled && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.ProxyHost)){
handler = CreateHandler(true, CrunchyrollManager.Instance.CrunOptions.ProxyHost, CrunchyrollManager.Instance.CrunOptions.ProxyPort);
Console.Error.WriteLine($"Proxy is set: http://{CrunchyrollManager.Instance.CrunOptions.ProxyHost}:{CrunchyrollManager.Instance.CrunOptions.ProxyPort}");
client = new HttpClient(handler);
} else if (systemProxy != null){
Uri testUri = new Uri("https://icanhazip.com");
Uri? proxyUri = systemProxy.GetProxy(testUri);
if (proxyUri != null && proxyUri != testUri){
if (proxyUri is{ Host: "127.0.0.1", Port: 7890 }){
Console.Error.WriteLine($"Proxy is set: {proxyUri}");
handler = CreateHandler(true);
} else{
Console.Error.WriteLine("No proxy will be used.");
handler = CreateHandler(false);
}
client = new HttpClient(handler);
} else{
Console.Error.WriteLine("No proxy is being used.");
client = new HttpClient(CreateHttpClientHandler());
}
} else{
Console.Error.WriteLine("No proxy is being used.");
client = new HttpClient(CreateHttpClientHandler());
}
// client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:126.0) Gecko/20100101 Firefox/126.0");
client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/1.9.0 Nintendo Switch/18.1.0.0 UE4/4.27");
@ -70,9 +102,24 @@ public class HttpClientReq{
};
}
private HttpClientHandler CreateHandler(bool useProxy, string? proxyHost = null, int proxyPort = 0){
var handler = new HttpClientHandler{
CookieContainer = new CookieContainer(),
UseCookies = true,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli,
UseProxy = useProxy
};
public void SetETPCookie(string refresh_token){
// var cookie = new Cookie("etp_rt", refresh_token){
if (useProxy && proxyHost != null){
handler.Proxy = new WebProxy($"http://{proxyHost}:{proxyPort}");
}
return handler;
}
public void SetETPCookie(string refreshToken){
// var cookie = new Cookie("etp_rt", refreshToken){
// Domain = "crunchyroll.com",
// Path = "/",
// };
@ -81,8 +128,11 @@ public class HttpClientReq{
// Domain = "crunchyroll.com",
// Path = "/",
// };
//
// handler.CookieContainer.Add(cookie);
// handler.CookieContainer.Add(cookie2);
AddCookie("crunchyroll.com", new Cookie("etp_rt", refresh_token));
AddCookie("crunchyroll.com", new Cookie("etp_rt", refreshToken));
AddCookie("crunchyroll.com", new Cookie("c_locale", "en-US"));
}

View file

@ -135,8 +135,7 @@ public class Merger{
audioIndex++;
}
args.Add("-acodec libmp3lame");
args.Add("-ab 192k");
args.Add("-c:a copy");
args.Add($"\"{options.Output}\"");
return string.Join(" ", args);
}
@ -190,6 +189,7 @@ public class Merger{
if (options.Subtitles.Count > 0){
foreach (var subObj in options.Subtitles){
bool isForced = false;
if (subObj.Delay.HasValue){
double delay = subObj.Delay ?? 0;
args.Add($"--sync 0:{delay}");
@ -206,10 +206,19 @@ public class Merger{
args.Add("--default-track 0");
if (CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay){
args.Add("--forced-track 0:yes");
isForced = true;
}
} else{
args.Add("--default-track 0:0");
}
if (subObj.ClosedCaption == true && CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag){
args.Add("--hearing-impaired-flag 0:yes");
}
if (subObj.Signs == true && CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced && !isForced){
args.Add("--forced-track 0:yes");
}
args.Add($"\"{subObj.File}\"");
}
@ -245,13 +254,21 @@ public class Merger{
public async Task<double> ProcessVideo(string baseVideoPath, string compareVideoPath){
var tempDir = Path.GetTempPath(); //TODO - maybe move this out of temp
var baseFramesDir = Path.Combine(tempDir, "base_frames");
var compareFramesDir = Path.Combine(tempDir, "compare_frames");
Directory.CreateDirectory(baseFramesDir);
Directory.CreateDirectory(compareFramesDir);
string baseFramesDir;
string compareFramesDir;
try{
var tempDir = CfgManager.PathTEMP_DIR;
baseFramesDir = Path.Combine(tempDir, "base_frames");
compareFramesDir = Path.Combine(tempDir, "compare_frames");
Directory.CreateDirectory(baseFramesDir);
Directory.CreateDirectory(compareFramesDir);
} catch (Exception e){
Console.Error.WriteLine(e);
return 0;
}
var extractFramesBase = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 60);
var extractFramesCompare = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 60);

View file

@ -69,9 +69,18 @@ public class CrDownloadOptions{
[YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)]
public bool IncludeSignsSubs{ get; set; }
[YamlMember(Alias = "mux_signs_subs_flag", ApplyNamingConventions = false)]
public bool SignsSubsAsForced{ get; set; }
[YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)]
public bool IncludeCcSubs{ get; set; }
[YamlMember(Alias = "cc_subs_font", ApplyNamingConventions = false)]
public string? CcSubsFont{ get; set; }
[YamlMember(Alias = "mux_cc_subs_flag", ApplyNamingConventions = false)]
public bool CcSubsMuxingFlag{ get; set; }
[YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)]
public bool Mp4{ get; set; }
@ -117,11 +126,17 @@ public class CrDownloadOptions{
[YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)]
public bool KeepDubsSeperate{ get; set; }
[YamlIgnore]
public bool? Skipmux{ get; set; }
[YamlMember(Alias = "mux_skip_muxing", ApplyNamingConventions = false)]
public bool SkipMuxing{ get; set; }
[YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)]
public bool SyncTiming{ get; set; }
[YamlMember(Alias = "encode_enabled", ApplyNamingConventions = false)]
public bool IsEncodeEnabled{ get; set; }
[YamlMember(Alias = "encode_preset", ApplyNamingConventions = false)]
public string? EncodingPresetName{ get; set; }
[YamlIgnore]
public bool Nocleanup{ get; set; }
@ -192,4 +207,12 @@ public class CrDownloadOptions{
[YamlMember(Alias = "download_speed_limit", ApplyNamingConventions = false)]
public int DownloadSpeedLimit{ get; set; }
[YamlMember(Alias = "proxy_enabled", ApplyNamingConventions = false)]
public bool ProxyEnabled{ get; set; }
[YamlMember(Alias = "proxy_host", ApplyNamingConventions = false)]
public string? ProxyHost{ get; set; }
[YamlMember(Alias = "proxy_port", ApplyNamingConventions = false)]
public int ProxyPort{ get; set; }
}

View file

@ -5,7 +5,7 @@ namespace CRD.Utils.Structs.Crunchyroll;
public class PlaybackData{
public int Total{ get; set; }
public List<Dictionary<string, Dictionary<string, StreamDetails>>>? Data{ get; set; }
public Dictionary<string, StreamDetails>? Data{ get; set; }
public PlaybackMeta? Meta{ get; set; }
}

View file

@ -10,7 +10,7 @@ public class StreamError{
public string Error{ get; set; }
[JsonPropertyName("activeStreams")]
public List<ActiveStream> ActiveStreams{ get; set; }
public List<ActiveStream> ActiveStreams{ get; set; } = new ();
public static StreamError? FromJson(string json){
try{

View file

@ -1,4 +1,5 @@
using System.ComponentModel;
using System.Collections.Generic;
using System.ComponentModel;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
@ -43,6 +44,12 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("sonarr_absolut_number")]
public string? SonarrAbsolutNumber{ get; set; }
[JsonProperty("history_episode_available_soft_subs")]
public List<string> HistoryEpisodeAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_episode_available_dub_lang")]
public List<string> HistoryEpisodeAvailableDubLang{ get; set; } =[];
public event PropertyChangedEventHandler? PropertyChanged;

View file

@ -317,6 +317,7 @@ public class HistorySeries : INotifyPropertyChanged{
}
public async Task FetchData(string? seasonId){
Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId);

View file

@ -12,7 +12,7 @@ public class Languages{
new(){ CrLocale = "en-US", Locale = "en", Code = "eng", Name = "English" },
new(){ CrLocale = "de-DE", Locale = "de", Code = "deu", Name = "German" },
new(){ CrLocale = "en-IN", Locale = "en-IN", Code = "eng", Name = "English (India)" },
new(){ CrLocale = "es-LA", Locale = "es-419", Code = "spa", Name = "Spanish", Language = "Latin American Spanish" },
new(){ CrLocale = "es-LA", Locale = "es-LA", Code = "spa", Name = "Spanish", Language = "Latin American Spanish" },
new(){ CrLocale = "es-419", Locale = "es-419", Code = "spa-419", Name = "Spanish", Language = "Latin American Spanish" },
new(){ CrLocale = "es-ES", Locale = "es-ES", Code = "spa-ES", Name = "Castilian", Language = "European Spanish" },
new(){ CrLocale = "pt-BR", Locale = "pt-BR", Code = "por", Name = "Portuguese", Language = "Brazilian Portuguese" },
@ -27,6 +27,7 @@ public class Languages{
// new(){ locale = "zh", code = "cmn", name = "Chinese (Mandarin, PRC)" },
new(){ CrLocale = "zh-CN", Locale = "zh-CN", Code = "zho", Name = "Chinese (Mainland China)" },
new(){ CrLocale = "zh-TW", Locale = "zh-TW", Code = "chi", Name = "Chinese (Taiwan)" },
new(){ CrLocale = "zh-HK", Locale = "zh-HK", Code = "zho-HK", Name = "Chinese (Hong Kong)" },
new(){ CrLocale = "ko-KR", Locale = "ko", Code = "kor", Name = "Korean" },
new(){ CrLocale = "ca-ES", Locale = "ca-ES", Code = "cat", Name = "Catalan" },
new(){ CrLocale = "pl-PL", Locale = "pl-PL", Code = "pol", Name = "Polish" },
@ -36,9 +37,28 @@ public class Languages{
new(){ CrLocale = "vi-VN", Locale = "vi-VN", Code = "vie", Name = "Vietnamese", Language = "Tiếng Việt" },
new(){ CrLocale = "id-ID", Locale = "id-ID", Code = "ind", Name = "Indonesian", Language = "Bahasa Indonesia" },
new(){ CrLocale = "te-IN", Locale = "te-IN", Code = "tel", Name = "Telugu (India)", Language = "తెలుగు" },
new(){ CrLocale = "id-ID", Locale = "id", Code = "in", Name = "Indonesian " }
};
public static List<string> SortListByLangList(List<string> langList){
var orderMap = languages.Select((value, index) => new { Value = value.CrLocale, Index = index })
.ToDictionary(x => x.Value, x => x.Index);
langList.Sort((x, y) =>
{
bool xExists = orderMap.ContainsKey(x);
bool yExists = orderMap.ContainsKey(y);
if (xExists && yExists)
return orderMap[x].CompareTo(orderMap[y]); // Sort by main list order
else if (xExists)
return -1; // x comes before any missing value
else if (yExists)
return 1; // y comes before any missing value
else
return string.Compare(x, y); // Sort alphabetically or by another logic for missing values
});
return langList;
}
public static LanguageItem FixAndFindCrLc(string cr_locale){
if (string.IsNullOrEmpty(cr_locale)){
@ -54,7 +74,7 @@ public class Languages{
string fileName = $"{fnOutput}";
if (addIndexAndLangCode){
fileName += $".{subsIndex}.{langItem.Code}";
fileName += $".{subsIndex}.{langItem.CrLocale}";
}
//removed .{langItem.language} from file name at end

View file

@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Avalonia.Data.Converters;
namespace CRD.Utils.UI;
public class UiListToStringConverter : IValueConverter{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture){
if (value is List<string> list){
return string.Join(", ", list);
}
return "";
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture){
if (value is string str){
return str.Split(new[]{ ", " }, StringSplitOptions.None).ToList();
}
return new List<string>();
}
}

View file

@ -11,13 +11,14 @@ using Newtonsoft.Json;
namespace CRD.Utils.Updater;
public class Updater : INotifyPropertyChanged{
public double progress = 0;
#region Singelton
private static Updater? _instance;
private static readonly object Padlock = new();
public double progress = 0;
public static Updater Instance{
get{
if (_instance == null){
@ -48,7 +49,9 @@ public class Updater : INotifyPropertyChanged{
public async Task<bool> CheckForUpdatesAsync(){
try{
using (var client = new HttpClient()){
HttpClientHandler handler = new HttpClientHandler();
handler.UseProxy = false;
using (var client = new HttpClient(handler)){
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
var response = await client.GetStringAsync(apiEndpoint);
var releaseInfo = Helpers.Deserialize<dynamic>(response,null);

View file

@ -4,9 +4,8 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Runtime;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
@ -17,11 +16,7 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using CRD.Views;
using DynamicData;
using FluentAvalonia.Core;
using ReactiveUI;
// ReSharper disable InconsistentNaming
namespace CRD.ViewModels;
@ -59,9 +54,9 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty]
private bool _searchPopupVisible = false;
public ObservableCollection<ItemModel> Items{ get; } = new();
public ObservableCollection<ItemModel> Items{ get; set; } = new();
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; set;} = new();
[ObservableProperty]
public CrBrowseSeries _selectedSearchItem;
@ -69,7 +64,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty]
public ComboBoxItem _currentSelectedSeason;
public ObservableCollection<ComboBoxItem> SeasonList{ get; } = new();
public ObservableCollection<ComboBoxItem> SeasonList{ get;set; } = new();
private Dictionary<string, List<ItemModel>> episodesBySeason = new();
@ -80,9 +75,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private CrunchyMusicVideoList? currentMusicVideoList;
private bool CurrentSeasonFullySelected = false;
private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1);
public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
}
@ -95,7 +88,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
return;
}
var searchResults = await CrunchyrollManager.Instance.CrSeries.Search(value, CrunchyrollManager.Instance.CrunOptions.HistoryLang);
var searchResults = await CrunchyrollManager.Instance.CrSeries.Search(value, CrunchyrollManager.Instance.CrunOptions.HistoryLang, true);
var searchItems = searchResults?.Data?.First().Items;
SearchItems.Clear();
@ -250,6 +243,11 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
//TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
}
private async Task HandleUrlInputAsync(){
@ -335,7 +333,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
var list = await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
id, DetermineLocale(locale),
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true));
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
SetLoadingState(false);
@ -428,6 +426,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){
if(SelectedItems == null) return;
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
@ -444,6 +443,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
if(Items == null) return;
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){
@ -510,7 +511,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
return await CrunchyrollManager.Instance.CrSeries.ListSeriesId(
seriesId,
locale,
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true));
new CrunchyMultiDownload(CrunchyrollManager.Instance.CrunOptions.DubLang, true), true);
}
private void SearchPopulateEpisodesBySeason(){
@ -518,6 +519,16 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
return;
}
Items.Clear();
SelectedItems.Clear();
episodesBySeason.Clear();
SeasonList.Clear();
//TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
foreach (var episode in currentSeriesList.Value.List){
var seasonKey = "S" + episode.Season;
var episodeModel = new ItemModel(
@ -584,6 +595,30 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonTextSelectSeason = "Select Season";
}
}
public void Dispose(){
foreach (var itemModel in Items){
itemModel.ImageBitmap?.Dispose(); // Dispose the bitmap if it exists
itemModel.ImageBitmap = null; // Nullify the reference to avoid lingering references
}
// Clear collections and other managed resources
Items.Clear();
Items = null;
SearchItems.Clear();
SearchItems = null;
SelectedItems.Clear();
SelectedItems = null;
SeasonList.Clear();
SeasonList = null;
episodesBySeason.Clear();
episodesBySeason = null;
selectedEpisodes.Clear();
selectedEpisodes = null;
}
}
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
@ -605,7 +640,7 @@ public class ItemModel(string id, string imageUrl, string description, string ti
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
ImageBitmap = await Helpers.LoadImage(url,208,117);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}

View file

@ -89,7 +89,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
if (epMeta.SelectedDubs == null || epMeta.SelectedDubs.Count < 1){
return "";
}
return epMeta.SelectedDubs.Aggregate("Dub: ", (current, crunOptionsDlDub) => current + (crunOptionsDlDub + " "));
}
@ -196,18 +196,7 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
}
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(ImageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ImageBitmap = new Bitmap(stream);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
ImageBitmap = await Helpers.LoadImage(ImageUrl, 208, 117);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}

View file

@ -29,10 +29,10 @@ namespace CRD.ViewModels;
public partial class HistoryPageViewModel : ViewModelBase{
public ObservableCollection<HistorySeries> Items{ get; }
public ObservableCollection<HistorySeries> FilteredItems{ get; }
[ObservableProperty]
private static bool _fetchingData;
private ProgramManager _programManager;
[ObservableProperty]
private HistorySeries _selectedSeries;
@ -112,6 +112,9 @@ public partial class HistoryPageViewModel : ViewModelBase{
private static string _progressText;
public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance;
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
} else{
@ -317,7 +320,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
[RelayCommand]
public void NavToSeries(){
if (FetchingData){
if (ProgramManager.FetchingData){
return;
}
@ -326,21 +329,21 @@ public partial class HistoryPageViewModel : ViewModelBase{
[RelayCommand]
public async Task RefreshAll(){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
ProgramManager.FetchingData = true;
RaisePropertyChanged(nameof(ProgramManager.FetchingData));
foreach (var item in FilteredItems){
item.SetFetchingData();
}
for (int i = 0; i < FilteredItems.Count; i++){
FetchingData = true;
RaisePropertyChanged(nameof(FetchingData));
ProgramManager.FetchingData = true;
RaisePropertyChanged(nameof(ProgramManager.FetchingData));
await FilteredItems[i].FetchData("");
FilteredItems[i].UpdateNewEpisodes();
}
FetchingData = false;
RaisePropertyChanged(nameof(FetchingData));
ProgramManager.FetchingData = false;
RaisePropertyChanged(nameof(ProgramManager.FetchingData));
CrunchyrollManager.Instance.History.SortItems();
}
@ -367,7 +370,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
public async Task AddMissingSonarrSeriesToHistory(){
SonarrOptionsOpen = false;
AddingMissingSonarrSeries = true;
FetchingData = true;
ProgramManager.FetchingData = true;
var crInstance = CrunchyrollManager.Instance;
@ -413,7 +416,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
ProgressText = "";
AddingMissingSonarrSeries = false;
FetchingData = false;
ProgramManager.FetchingData = false;
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}

View file

@ -1,75 +1,15 @@
using System;
using System.IO;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls.Chrome;
using Avalonia.Media;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
using FluentAvalonia.Styling;
using Newtonsoft.Json;
namespace CRD.ViewModels;
public partial class MainWindowViewModel : ViewModelBase{
private readonly FluentAvaloniaTheme _faTheme;
[ObservableProperty]
private bool _updateAvailable = true;
public ProgramManager _programManager;
[ObservableProperty]
private bool _finishedLoading = false;
public MainWindowViewModel(){
_faTheme = App.Current.Styles[0] as FluentAvaloniaTheme;
Init();
CleanUpOldUpdater();
}
private void CleanUpOldUpdater(){
string backupFilePath = Path.Combine(Directory.GetCurrentDirectory(), "Updater.exe.bak");
if (File.Exists(backupFilePath)){
try{
File.Delete(backupFilePath);
Console.WriteLine($"Deleted old updater file: {backupFilePath}");
} catch (Exception ex){
Console.Error.WriteLine($"Failed to delete old updater file: {ex.Message}");
}
} else{
Console.WriteLine("No old updater file found to delete.");
}
}
public async void Init(){
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
CrunchyrollManager.Instance.InitOptions();
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null){
_faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
}
if (CrunchyrollManager.Instance.CrunOptions.Theme == "System"){
_faTheme.PreferSystemTheme = true;
} else if (CrunchyrollManager.Instance.CrunOptions.Theme == "Dark"){
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
} else{
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
await CrunchyrollManager.Instance.Init();
FinishedLoading = true;
public MainWindowViewModel(ProgramManager manager){
ProgramManager = manager;
}
}

View file

@ -1,15 +1,18 @@
using System;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
@ -18,6 +21,7 @@ using CRD.Views;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
using Path = Avalonia.Controls.Shapes.Path;
namespace CRD.ViewModels;
@ -42,6 +46,12 @@ public partial class SeriesPageViewModel : ViewModelBase{
[ObservableProperty]
private string _availableSubs;
[ObservableProperty]
private string _seriesFolderPath;
[ObservableProperty]
public bool _seriesFolderPathExists;
public SeriesPageViewModel(){
_selectedSeries = CrunchyrollManager.Instance.SelectedSeries;
@ -63,6 +73,58 @@ public partial class SeriesPageViewModel : ViewModelBase{
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
UpdateSeriesFolderPath();
}
private void UpdateSeriesFolderPath(){
var season = SelectedSeries.Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath));
if (!string.IsNullOrEmpty(SelectedSeries.SeriesDownloadPath) && Directory.Exists(SelectedSeries.SeriesDownloadPath)){
SeriesFolderPath = SelectedSeries.SeriesDownloadPath;
SeriesFolderPathExists = true;
}
if (season is{ SeasonDownloadPath: not null }){
try{
var seasonPath = season.SeasonDownloadPath;
var directoryInfo = new DirectoryInfo(seasonPath);
string parentFolderPath = directoryInfo.Parent?.FullName;
if (Directory.Exists(parentFolderPath)){
SeriesFolderPath = parentFolderPath;
SeriesFolderPathExists = true;
}
} catch (Exception e){
Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}");
}
} else{
var customPath = string.Empty;
if (string.IsNullOrEmpty(SelectedSeries.SeriesTitle))
return;
var seriesTitle = FileNameManager.CleanupFilename(SelectedSeries.SeriesTitle);
if (string.IsNullOrEmpty(seriesTitle))
return;
// Check Crunchyroll download directory
var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath;
if (!string.IsNullOrEmpty(downloadDirPath)){
customPath = System.IO.Path.Combine(downloadDirPath, seriesTitle);
} else{
// Fallback to configured VIDEOS_DIR path
customPath = System.IO.Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
}
// Check if custom path exists
if (Directory.Exists(customPath)){
SeriesFolderPath = customPath;
SeriesFolderPathExists = true;
}
}
}
[RelayCommand]
@ -90,6 +152,8 @@ public partial class SeriesPageViewModel : ViewModelBase{
CfgManager.UpdateHistoryFile();
}
}
UpdateSeriesFolderPath();
}
public void SetStorageProvider(IStorageProvider storageProvider){
@ -105,7 +169,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
FullSizeDesired = true
};
var viewModel = new ContentDialogSonarrMatchViewModel(dialog, SelectedSeries.SonarrSeriesId,SelectedSeries.SeriesTitle);
var viewModel = new ContentDialogSonarrMatchViewModel(dialog, SelectedSeries.SonarrSeriesId, SelectedSeries.SeriesTitle);
dialog.Content = new ContentDialogSonarrMatchView(){
DataContext = viewModel
};
@ -116,10 +180,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.SonarrSeriesId = viewModel.CurrentSonarrSeries.Id.ToString();
SelectedSeries.SonarrTvDbId = viewModel.CurrentSonarrSeries.TvdbId.ToString();
SelectedSeries.SonarrSlugTitle = viewModel.CurrentSonarrSeries.TitleSlug;
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrConnected = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
if (!string.IsNullOrEmpty(SelectedSeries.SonarrSeriesId)){
SonarrAvailable = SelectedSeries.SonarrSeriesId.Length > 0 && SonarrConnected;
} else{
@ -131,7 +195,6 @@ public partial class SeriesPageViewModel : ViewModelBase{
UpdateData("");
}
}
@ -147,9 +210,13 @@ public partial class SeriesPageViewModel : ViewModelBase{
public async Task DownloadSeasonMissing(HistorySeason season){
var downloadTasks = season.EpisodesList
.Where(episode => !episode.WasDownloaded)
.Select(episode => episode.DownloadEpisode());
.Select(episode => episode.DownloadEpisode()).ToList();
await Task.WhenAll(downloadTasks);
if (downloadTasks.Count == 0){
MessageBus.Current.SendMessage(new ToastMessage($"There are no missing episodes", ToastType.Error, 3));
} else{
await Task.WhenAll(downloadTasks);
}
}
[RelayCommand]
@ -190,4 +257,18 @@ public partial class SeriesPageViewModel : ViewModelBase{
SelectedSeries.UpdateNewEpisodes();
MessageBus.Current.SendMessage(new NavigationMessage(null, true, false));
}
[RelayCommand]
public void OpenFolderPath(){
try{
Process.Start(new ProcessStartInfo{
FileName = SeriesFolderPath,
UseShellExecute = true,
Verb = "open"
});
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
}
}
}

View file

@ -5,7 +5,7 @@ using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Mime;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
@ -15,14 +15,18 @@ using Avalonia.Platform.Storage;
using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.CustomList;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils;
using CRD.Views.Utils;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls;
// ReSharper disable InconsistentNaming
namespace CRD.ViewModels;
@ -40,7 +44,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
private bool _downloadChapters = true;
[ObservableProperty]
private bool _addScaledBorderAndShadow = false;
private bool _addScaledBorderAndShadow;
[ObservableProperty]
private bool _includeSignSubs;
@ -59,6 +63,9 @@ public partial class SettingsPageViewModel : ViewModelBase{
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" },
};
[ObservableProperty]
private bool _skipMuxing;
[ObservableProperty]
private bool _muxToMp4;
@ -108,7 +115,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
private string _fileTitle = "";
[ObservableProperty]
private ObservableCollection<MuxingParam> _mkvMergeOptions = new();
private ObservableCollection<StringItem> _mkvMergeOptions = new();
[ObservableProperty]
private string _mkvMergeOption = "";
@ -117,7 +124,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
private string _ffmpegOption = "";
[ObservableProperty]
private ObservableCollection<MuxingParam> _ffmpegOptions = new();
private ObservableCollection<StringItem> _ffmpegOptions = new();
[ObservableProperty]
private string _selectedSubs = "all";
@ -327,12 +334,44 @@ public partial class SettingsPageViewModel : ViewModelBase{
new ComboBoxItem(){ Content = "tv/samsung" },
};
[ObservableProperty]
private bool _isEncodeEnabled;
[ObservableProperty]
private StringItem _selectedEncodingPreset;
public ObservableCollection<StringItem> EncodingPresetsList{ get; } = new();
[ObservableProperty]
private string _downloadDirPath;
[ObservableProperty]
private bool _proxyEnabled;
[ObservableProperty]
private string _proxyHost;
[ObservableProperty]
private double? _proxyPort;
[ObservableProperty]
private string _tempDownloadDirPath;
[ObservableProperty]
private string _currentIp = "";
[ObservableProperty]
private bool _cCSubsMuxingFlag;
[ObservableProperty]
private string _cCSubsFont;
[ObservableProperty]
private bool _signsSubsAsForced;
private readonly FluentAvaloniaTheme _faTheme;
private bool settingsLoaded;
@ -345,6 +384,12 @@ public partial class SettingsPageViewModel : ViewModelBase{
_faTheme = App.Current.Styles[0] as FluentAvaloniaTheme;
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
} else{
CustomAccentColor = Application.Current?.PlatformSettings?.GetColorValues().AccentColor1 ?? Colors.SlateBlue;
}
foreach (var languageItem in Languages.languages){
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
@ -353,11 +398,19 @@ public partial class SettingsPageViewModel : ViewModelBase{
DefaultSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
}
foreach (var encodingPreset in FfmpegEncoding.presets){
EncodingPresetsList.Add(new StringItem{ stringValue = encodingPreset.PresetName ?? "Unknown Preset Name" });
}
CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions;
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == options.EncodingPresetName) ?? null;
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
ComboBoxItem? descriptionLang = DescriptionLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.DescriptionLang) ?? null;
SelectedDescriptionLang = descriptionLang ?? DescriptionLangList[0];
@ -403,6 +456,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
CCSubsFont = options.CcSubsFont ?? "";
CCSubsMuxingFlag = options.CcSubsMuxingFlag;
SignsSubsAsForced = options.SignsSubsAsForced;
ProxyEnabled = options.ProxyEnabled;
ProxyHost = options.ProxyHost ?? "";
ProxyPort = options.ProxyPort;
SkipMuxing = options.SkipMuxing;
IsEncodeEnabled = options.IsEncodeEnabled;
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay;
DefaultSubSigns = options.DefaultSubSigns;
HistoryAddSpecials = options.HistoryAddSpecials;
@ -435,7 +496,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
CurrentAppTheme = theme ?? AppThemes[0];
if (options.AccentColor != CustomAccentColor.ToString()){
if (!string.IsNullOrEmpty(options.AccentColor) && options.AccentColor != Application.Current?.PlatformSettings?.GetColorValues().AccentColor1.ToString()){
UseCustomAccent = true;
}
@ -444,14 +505,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){
foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new MuxingParam(){ ParamValue = mkvmergeParam });
MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam });
}
}
FfmpegOptions.Clear();
if (options.FfmpegOptions != null){
foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new MuxingParam(){ ParamValue = ffmpegParam });
FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam });
}
}
@ -475,6 +536,11 @@ public partial class SettingsPageViewModel : ViewModelBase{
return;
}
CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced = SignsSubsAsForced;
CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag = CCSubsMuxingFlag;
CrunchyrollManager.Instance.CrunOptions.CcSubsFont = CCSubsFont;
CrunchyrollManager.Instance.CrunOptions.EncodingPresetName = SelectedEncodingPreset.stringValue;
CrunchyrollManager.Instance.CrunOptions.IsEncodeEnabled = IsEncodeEnabled;
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns = DefaultSubSigns;
CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay;
@ -487,6 +553,7 @@ public partial class SettingsPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DlVideoOnce = !DownloadVideoForEveryDub;
CrunchyrollManager.Instance.CrunOptions.KeepDubsSeperate = KeepDubsSeparate;
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing;
CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4;
CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings;
CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
@ -497,6 +564,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.DownloadSpeedLimit = Math.Clamp((int)(DownloadSpeed ?? 0), 0, 1000000000);
CrunchyrollManager.Instance.CrunOptions.SimultaneousDownloads = Math.Clamp((int)(SimultaneousDownloads ?? 0), 1, 10);
CrunchyrollManager.Instance.CrunOptions.ProxyEnabled = ProxyEnabled;
CrunchyrollManager.Instance.CrunOptions.ProxyHost = ProxyHost;
CrunchyrollManager.Instance.CrunOptions.ProxyPort = Math.Clamp((int)(ProxyPort ?? 0), 0, 65535);
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
List<string> softSubs = new List<string>();
@ -535,7 +606,11 @@ public partial class SettingsPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + "";
CrunchyrollManager.Instance.CrunOptions.AccentColor = _faTheme.CustomAccentColor.ToString();
if (_faTheme.CustomAccentColor != (Application.Current?.PlatformSettings?.GetColorValues().AccentColor1)){
CrunchyrollManager.Instance.CrunOptions.AccentColor = _faTheme.CustomAccentColor.ToString();
} else{
CrunchyrollManager.Instance.CrunOptions.AccentColor = string.Empty;
}
CrunchyrollManager.Instance.CrunOptions.History = History;
@ -560,14 +635,14 @@ public partial class SettingsPageViewModel : ViewModelBase{
List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.ParamValue);
mkvmergeParams.Add(mkvmergeParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams;
List<string> ffmpegParams = new List<string>();
foreach (var ffmpegParam in FfmpegOptions){
ffmpegParams.Add(ffmpegParam.ParamValue);
ffmpegParams.Add(ffmpegParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
@ -605,26 +680,26 @@ public partial class SettingsPageViewModel : ViewModelBase{
[RelayCommand]
public void AddMkvMergeParam(){
MkvMergeOptions.Add(new MuxingParam(){ ParamValue = MkvMergeOption });
MkvMergeOptions.Add(new StringItem(){ stringValue = MkvMergeOption });
MkvMergeOption = "";
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void RemoveMkvMergeParam(MuxingParam param){
public void RemoveMkvMergeParam(StringItem param){
MkvMergeOptions.Remove(param);
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void AddFfmpegParam(){
FfmpegOptions.Add(new MuxingParam(){ ParamValue = FfmpegOption });
FfmpegOptions.Add(new StringItem(){ stringValue = FfmpegOption });
FfmpegOption = "";
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public void RemoveFfmpegParam(MuxingParam param){
public void RemoveFfmpegParam(StringItem param){
FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions));
}
@ -632,7 +707,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
[RelayCommand]
public async Task OpenFolderDialogAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path,
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path;
DownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathVIDEOS_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath,
defaultPath: CfgManager.PathVIDEOS_DIR
);
@ -641,7 +719,10 @@ public partial class SettingsPageViewModel : ViewModelBase{
[RelayCommand]
public async Task OpenFolderDialogTempFolderAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path,
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path;
TempDownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathTEMP_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath,
defaultPath: CfgManager.PathTEMP_DIR
);
@ -697,7 +778,8 @@ public partial class SettingsPageViewModel : ViewModelBase{
} else{
CustomAccentColor = default;
ListBoxColor = default;
UpdateAppAccentColor(Colors.SlateBlue);
var color = Application.Current?.PlatformSettings?.GetColorValues().AccentColor1 ?? Colors.SlateBlue;
UpdateAppAccentColor(color);
}
}
@ -734,7 +816,13 @@ public partial class SettingsPageViewModel : ViewModelBase{
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
base.OnPropertyChanged(e);
if (e.PropertyName is nameof(SelectedDubs) or nameof(SelectedSubs) or nameof(CustomAccentColor) or nameof(ListBoxColor) or nameof(CurrentAppTheme) or nameof(UseCustomAccent) or nameof(LogMode)){
if (e.PropertyName is nameof(SelectedDubs)
or nameof(SelectedSubs)
or nameof(CustomAccentColor)
or nameof(ListBoxColor)
or nameof(CurrentAppTheme)
or nameof(UseCustomAccent)
or nameof(LogMode)){
return;
}
@ -766,6 +854,45 @@ public partial class SettingsPageViewModel : ViewModelBase{
}
}
[RelayCommand]
public async Task CreateEncodingPresetButtonPress(bool editMode){
var dialog = new ContentDialog(){
Title = "New Encoding Preset",
PrimaryButtonText = "Save",
CloseButtonText = "Close",
FullSizeDesired = true
};
var viewModel = new ContentDialogEncodingPresetViewModel(dialog, editMode);
dialog.Content = new ContentDialogEncodingPresetView(){
DataContext = viewModel
};
var dialogResult = await dialog.ShowAsync();
if (dialogResult == ContentDialogResult.Primary){
settingsLoaded = false;
EncodingPresetsList.Clear();
foreach (var encodingPreset in FfmpegEncoding.presets){
EncodingPresetsList.Add(new StringItem{ stringValue = encodingPreset.PresetName ?? "Unknown Preset Name" });
}
settingsLoaded = true;
StringItem? encodingPresetSelected = EncodingPresetsList.FirstOrDefault(a => a.stringValue != null && a.stringValue == CrunchyrollManager.Instance.CrunOptions.EncodingPresetName) ?? null;
SelectedEncodingPreset = encodingPresetSelected ?? EncodingPresetsList[0];
}
}
[RelayCommand]
public async void CheckIp(){
var result = await HttpClientReq.Instance.SendHttpRequest(HttpClientReq.CreateRequestMessage("https://icanhazip.com", HttpMethod.Get, false, false, null));
Console.Error.WriteLine("Your IP: " + result.ResponseContent);
if (result.IsOk){
CurrentIp = result.ResponseContent;
}
}
partial void OnLogModeChanged(bool value){
UpdateSettings();
if (value){
@ -774,8 +901,4 @@ public partial class SettingsPageViewModel : ViewModelBase{
CfgManager.DisableLogMode();
}
}
}
public class MuxingParam{
public string ParamValue{ get; set; }
}

View file

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Structs;
using CRD.Views;
using DynamicData;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogEncodingPresetViewModel : ViewModelBase{
private readonly ContentDialog dialog;
[ObservableProperty]
private bool _editMode;
[ObservableProperty]
private string _presetName;
[ObservableProperty]
private string _codec;
[ObservableProperty]
private ComboBoxItem _selectedResolution = new();
[ObservableProperty]
private double? _crf = 23;
[ObservableProperty]
private double? _frameRate = 30;
[ObservableProperty]
private string _additionalParametersString = "";
[ObservableProperty]
private ObservableCollection<StringItem> _additionalParameters = new();
[ObservableProperty]
private VideoPreset? _selectedCustomPreset;
[ObservableProperty]
private bool _fileExists;
public ObservableCollection<VideoPreset> CustomPresetsList{ get; } = new(){ };
public ObservableCollection<ComboBoxItem> ResolutionList{ get; } = new(){
new ComboBoxItem(){ Content = "3840:2160" }, // 4K UHD
new ComboBoxItem(){ Content = "3440:1440" }, // Ultra-Wide Quad HD
new ComboBoxItem(){ Content = "2560:1440" }, // 1440p
new ComboBoxItem(){ Content = "2560:1080" }, // Ultra-Wide Full HD
new ComboBoxItem(){ Content = "2160:1080" }, // 2:1 Aspect Ratio
new ComboBoxItem(){ Content = "1920:1080" }, // 1080p Full HD
new ComboBoxItem(){ Content = "1920:800" }, // Cinematic 2.40:1
new ComboBoxItem(){ Content = "1600:900" }, // 900p
new ComboBoxItem(){ Content = "1366:768" }, // 768p
new ComboBoxItem(){ Content = "1280:960" }, // SXGA 4:3
new ComboBoxItem(){ Content = "1280:720" }, // 720p HD
new ComboBoxItem(){ Content = "1024:576" }, // 576p
new ComboBoxItem(){ Content = "960:540" }, // 540p qHD
new ComboBoxItem(){ Content = "854:480" }, // 480p
new ComboBoxItem(){ Content = "800:600" }, // SVGA
new ComboBoxItem(){ Content = "768:432" }, // 432p
new ComboBoxItem(){ Content = "720:480" }, // NTSC SD
new ComboBoxItem(){ Content = "704:576" }, // PAL SD
new ComboBoxItem(){ Content = "640:360" }, // 360p
new ComboBoxItem(){ Content = "426:240" }, // 240p
new ComboBoxItem(){ Content = "320:240" }, // QVGA
new ComboBoxItem(){ Content = "320:180" }, // 180p
new ComboBoxItem(){ Content = "256:144" }, // 144p
};
public ContentDialogEncodingPresetViewModel(ContentDialog dialog, bool editMode){
this.dialog = dialog;
if (dialog is null){
throw new ArgumentNullException(nameof(dialog));
}
if (editMode){
EditMode = true;
CustomPresetsList.AddRange(FfmpegEncoding.presets.Skip(15));
this.dialog.Title = "Edit Encoding Preset";
if (CustomPresetsList.Count == 0){
MessageBus.Current.SendMessage(new ToastMessage($"There are no presets to be edited", ToastType.Warning, 5));
EditMode = false;
} else{
SelectedCustomPreset = CustomPresetsList.First();
}
}
dialog.Closed += DialogOnClosed;
dialog.PrimaryButtonClick += SaveButton;
}
partial void OnSelectedCustomPresetChanged(VideoPreset value){
PresetName = value.PresetName ?? "";
Codec = value.Codec ?? "";
Crf = value.Crf;
FrameRate = double.Parse(value.FrameRate ?? "0");
SelectedResolution = ResolutionList.FirstOrDefault(e => e.Content?.ToString() == value.Resolution) ?? ResolutionList.First();
AdditionalParameters.Clear();
foreach (var valueAdditionalParameter in value.AdditionalParameters){
AdditionalParameters.Add(new StringItem(){ stringValue = valueAdditionalParameter });
}
AdditionalParametersString = "";
}
partial void OnPresetNameChanged(string value){
var path = Path.Combine(CfgManager.PathENCODING_PRESETS_DIR, value + ".json");
var fileExists = File.Exists(path);
dialog.IsPrimaryButtonEnabled = !fileExists || EditMode && value == SelectedCustomPreset?.PresetName;
FileExists = !dialog.IsPrimaryButtonEnabled;
}
[RelayCommand]
public void AddAdditionalParam(){
AdditionalParameters.Add(new StringItem(){ stringValue = AdditionalParametersString });
AdditionalParametersString = "";
RaisePropertyChanged(nameof(AdditionalParametersString));
}
[RelayCommand]
public void RemoveAdditionalParam(StringItem param){
AdditionalParameters.Remove(param);
RaisePropertyChanged(nameof(AdditionalParameters));
}
private void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
dialog.PrimaryButtonClick -= SaveButton;
if (EditMode){
if (SelectedCustomPreset != null){
var oldName = SelectedCustomPreset.PresetName;
SelectedCustomPreset.PresetName = PresetName;
SelectedCustomPreset.Codec = Codec;
SelectedCustomPreset.FrameRate = Math.Clamp((int)(FrameRate ?? 1), 1, 999).ToString();
SelectedCustomPreset.Crf = Math.Clamp((int)(Crf ?? 0), 0, 51);
SelectedCustomPreset.Resolution = SelectedResolution.Content?.ToString() ?? "1920:1080";
SelectedCustomPreset.AdditionalParameters = AdditionalParameters.Select(additionalParameter => additionalParameter.stringValue).ToList();
try{
var oldPath = Path.Combine(CfgManager.PathENCODING_PRESETS_DIR, oldName + ".json");
var path = Path.Combine(CfgManager.PathENCODING_PRESETS_DIR, SelectedCustomPreset.PresetName + ".json");
if (File.Exists(oldPath)){
File.Delete(oldPath);
}
CfgManager.WriteJsonToFile(path, SelectedCustomPreset);
} catch (Exception e){
Console.Error.WriteLine("Error saving preset: " + e);
}
}
} else{
VideoPreset newPreset = new VideoPreset(){
PresetName = PresetName,
Codec = Codec,
FrameRate = Math.Clamp((int)(FrameRate ?? 1), 1, 999).ToString(),
Crf = Math.Clamp((int)(Crf ?? 0), 0, 51),
Resolution = SelectedResolution.Content?.ToString() ?? "1920:1080",
AdditionalParameters = AdditionalParameters.Select(additionalParameter => additionalParameter.stringValue).ToList()
};
CfgManager.WriteJsonToFile(Path.Combine(CfgManager.PathENCODING_PRESETS_DIR, newPreset.PresetName + ".json"), newPreset);
FfmpegEncoding.AddPreset(newPreset);
}
}
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
dialog.Closed -= DialogOnClosed;
}
}

View file

@ -5,13 +5,21 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
xmlns:ui="clr-namespace:CRD.Utils.UI"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
x:DataType="vm:AddDownloadPageViewModel"
x:Class="CRD.Views.AddDownloadPageView">
x:Class="CRD.Views.AddDownloadPageView"
Unloaded="OnUnloaded">
<Design.DataContext>
<vm:AddDownloadPageViewModel />
</Design.DataContext>
<UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
</UserControl.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- For the TextBox -->
@ -115,8 +123,8 @@
<CheckBox IsEnabled="{Binding AllButtonEnabled}" IsChecked="{Binding AddAllEpisodes}"
Content="All" Margin="5 0 0 0">
</CheckBox>
<Button IsVisible="{Binding SlectSeasonVisible}" IsEnabled="{Binding !ShowLoading}" Width="200" Command="{Binding OnSelectSeasonPressed}"
<Button IsVisible="{Binding SlectSeasonVisible}" IsEnabled="{Binding !ShowLoading}" Width="200" Command="{Binding OnSelectSeasonPressed}"
Content="{Binding ButtonTextSelectSeason}">
</Button>
@ -135,33 +143,42 @@
<Grid Grid.Row="2">
<!-- Spinner Style ProgressBar -->
<ProgressBar IsIndeterminate="True"
Value="50"
Maximum="100"
MaxWidth="100"
IsVisible="{Binding ShowLoading}">
</ProgressBar>
<controls:ProgressRing IsVisible="{Binding ShowLoading}" Width="100" Height="100" HorizontalAlignment="Center" VerticalAlignment="Center" />
</Grid>
<!-- ListBox with Custom Elements -->
<ListBox Grid.Row="2" Margin="10" SelectionMode="Multiple,Toggle" VerticalAlignment="Stretch"
SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}" x:Name="Grid">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Vertical" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ItemModel}">
<DataTemplate>
<StackPanel>
<Border Padding="10" Margin="5" BorderThickness="1">
<Border Padding="10 0" Margin="5" BorderThickness="1">
<Grid Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" /> <!-- Top content takes available space -->
<RowDefinition Height="Auto" /> <!-- Bottom row for AvailableAudios TextBlock -->
</Grid.RowDefinitions>
<!-- Image -->
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
<Image Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" Width="208" Height="117" Source="{Binding ImageBitmap}"
Stretch="Fill" />
<!-- <Image Grid.Column="0" Grid.Row="0" Grid.RowSpan="2" asyncImageLoader:ImageLoader.Source="{Binding ImageUrl}" Width="208" Height="117" Stretch="Fill"></Image> -->
<!-- Text Content -->
<Grid Grid.Column="1" Margin="10" VerticalAlignment="Top">
<Grid Grid.Column="1" Grid.Row="0" Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title -->
<ColumnDefinition Width="Auto" />
@ -179,7 +196,19 @@
<TextBlock Grid.Column="0" Grid.Row="1" Grid.ColumnSpan="2"
Text="{Binding Description}"
FontStyle="Italic" Opacity="0.8" TextWrapping="Wrap" />
</Grid>
<StackPanel Grid.Row="1" Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center" Margin="10">
<TextBlock FontStyle="Italic"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<TextBlock Text="{Binding AvailableAudios, Converter={StaticResource UiListToStringConverter}}"
FontStyle="Italic"
Opacity="0.8"
HorizontalAlignment="Left"
VerticalAlignment="Bottom"
TextWrapping="NoWrap" />
</StackPanel>
</Grid>
</Border>
<Border Background="LightGray" Height="1" Margin="0,5" HorizontalAlignment="Stretch" />

View file

@ -1,7 +1,7 @@
using System;
using Avalonia;
using System.Runtime;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Interactivity;
using CRD.ViewModels;
namespace CRD.Views;
@ -11,6 +11,15 @@ public partial class AddDownloadPageView : UserControl{
InitializeComponent();
}
private void OnUnloaded(object? sender, RoutedEventArgs e){
if (DataContext is AddDownloadPageViewModel viewModel){
viewModel.Dispose();
DataContext = null;
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
}
}
private void Popup_Closed(object? sender, EventArgs e){
if (DataContext is AddDownloadPageViewModel viewModel){
viewModel.SearchPopupVisible = false;

View file

@ -39,8 +39,15 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Image -->
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
Stretch="Fill" />
<!-- <Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}" -->
<!-- Stretch="Fill" /> -->
<Grid>
<Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" />
<Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}"
Stretch="Fill" />
</Grid>
<!-- Text Content -->
<Grid Grid.Column="1" Margin="10" >

View file

@ -37,7 +37,7 @@
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
Command="{Binding RefreshAll}"
IsEnabled="{Binding !FetchingData}">
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sync" FontSize="32" />
<TextBlock Text="Refresh Filtered" TextWrapping="Wrap" FontSize="12"></TextBlock>
@ -47,7 +47,7 @@
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
Command="{Binding AddMissingToQueue}"
IsEnabled="{Binding !FetchingData}">
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Import" FontSize="32" />
<TextBlock Text="Add To Queue" TextWrapping="Wrap" FontSize="12"></TextBlock>
@ -57,7 +57,7 @@
<ToggleButton Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsChecked="{Binding EditMode}"
IsEnabled="{Binding !FetchingData}">
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Edit" FontSize="32" />
<TextBlock Text="Edit" TextWrapping="Wrap" FontSize="12"></TextBlock>
@ -71,7 +71,7 @@
BorderThickness="0" CornerRadius="5"
IsVisible="{Binding SonarrAvailable}"
IsChecked="{Binding SonarrOptionsOpen}"
IsEnabled="{Binding !FetchingData}">
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:ImageIcon VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 1 0 0" Source="../Assets/sonarr.png" Width="30" Height="30" />
<TextBlock Text="Sonarr" TextWrapping="Wrap" FontSize="12"></TextBlock>
@ -119,7 +119,7 @@
<StackPanel>
<ToggleButton x:Name="DropdownButtonViews" Width="70" Height="70" Background="Transparent"
BorderThickness="0" Margin="5 0" VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}"
IsEnabled="{Binding !ProgramManager.FetchingData}"
IsChecked="{Binding ViewSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="View" FontSize="32" />
@ -143,7 +143,7 @@
<StackPanel>
<ToggleButton x:Name="DropdownButtonSorting" Width="70" Height="70" Background="Transparent"
BorderThickness="0" Margin="5 0" VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}"
IsEnabled="{Binding !ProgramManager.FetchingData}"
IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="32" />
@ -175,7 +175,7 @@
<StackPanel>
<ToggleButton x:Name="DropdownButtonFilter" Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
VerticalAlignment="Center"
IsEnabled="{Binding !FetchingData}">
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock>
@ -273,7 +273,7 @@
FontStyle="Italic"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}">
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).ProgramManager.FetchingData}">
<ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip>
@ -872,7 +872,7 @@
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).FetchingData}">
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).ProgramManager.FetchingData}">
<ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip>

View file

@ -60,12 +60,12 @@
IconSource="Add" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Calendar" Tag="Calendar"
IconSource="Calendar" />
<ui:NavigationViewItem IsEnabled="{Binding FinishedLoading}" Classes="SampleAppNav" Content="History" Tag="History"
<ui:NavigationViewItem IsEnabled="{Binding ProgramManager.FinishedLoading}" Classes="SampleAppNav" Content="History" Tag="History"
IconSource="Library" />
</ui:NavigationView.MenuItems>
<ui:NavigationView.FooterMenuItems>
<ui:NavigationViewItem Classes="SampleAppNav" Content="Update Available" Tag="UpdateAvailable"
IconSource="CloudDownload" Focusable="False" IsEnabled="{Binding UpdateAvailable}" />
IconSource="CloudDownload" Focusable="False" IsEnabled="{Binding ProgramManager.UpdateAvailable}" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Account" Tag="Account"
IconSource="Contact" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Settings" Tag="Settings"

View file

@ -75,7 +75,7 @@ public partial class MainWindow : AppWindow{
//select first element as default
var nv = this.FindControl<NavigationView>("NavView");
nv.SelectedItem = IEnumerableExtensions.ElementAt(nv.MenuItems, 0);
nv.SelectedItem = nv.MenuItems.ElementAt(0);
selectedNavVieItem = nv.SelectedItem;
MessageBus.Current.Listen<NavigationMessage>()

View file

@ -11,6 +11,9 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.SeriesPageView">
<UserControl.Resources>
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
</UserControl.Resources>
<Grid>
<Grid Margin="10">
@ -31,11 +34,11 @@
<StackPanel Orientation="Vertical">
<Grid Margin="10" VerticalAlignment="Top" >
<Grid Margin="10" VerticalAlignment="Top">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
</Grid.ColumnDefinitions>
<Image Grid.Column="0" Margin="10" Source="{Binding SelectedSeries.ThumbnailImage}" Width="240"
Height="360">
</Image>
@ -63,6 +66,9 @@
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
Command="{Binding SelectedSeries.OpenCrPage}">
<ToolTip.Tip>
<TextBlock Text="Open Crunchyroll Webpage" FontSize="15" />
</ToolTip.Tip>
<Grid>
<controls:ImageIcon Source="../Assets/crunchy_icon_round.png" Width="30" Height="30" />
</Grid>
@ -72,11 +78,27 @@
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SonarrAvailable}"
Command="{Binding SelectedSeries.OpenSonarrPage}">
<ToolTip.Tip>
<TextBlock Text="Open Sonarr Webpage" FontSize="15" />
</ToolTip.Tip>
<Grid>
<controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" />
</Grid>
</Button>
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
BorderThickness="0" CornerRadius="50"
IsVisible="{Binding SeriesFolderPathExists}"
Command="{Binding OpenFolderPath}">
<ToolTip.Tip>
<TextBlock Text="Open Series Folder" FontSize="15" />
</ToolTip.Tip>
<Grid>
<!-- <controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" /> -->
<controls:SymbolIcon Symbol="Folder" FontSize="20" Width="30" Height="30" />
</Grid>
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal">
@ -202,10 +224,10 @@
</Border>
</Popup>
</StackPanel>
<StackPanel IsVisible="{Binding EditMode}">
<Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0"
<Button Width="30" Height="30" Margin="0 0 10 0"
BorderThickness="0"
IsVisible="{Binding SonarrConnected}"
Command="{Binding MatchSonarrSeries_Button}">
<Grid>
@ -213,7 +235,6 @@
</Grid>
</Button>
</StackPanel>
</StackPanel>
@ -247,15 +268,35 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
<StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="E"></TextBlock>
<TextBlock Text="{Binding Episode}"></TextBlock>
<TextBlock Text=" - "></TextBlock>
<TextBlock Text="{Binding EpisodeTitle}"></TextBlock>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="Dubs: ">
</TextBlock>
<TextBlock FontStyle="Italic"
FontSize="12"
Opacity="0.8" Text="{Binding HistoryEpisodeAvailableDubLang, Converter={StaticResource UiListToStringConverter}}" />
</StackPanel>
<!-- <StackPanel Orientation="Horizontal" VerticalAlignment="Center"> -->
<!-- <TextBlock FontStyle="Italic" -->
<!-- FontSize="12" -->
<!-- Opacity="0.8" Text="Subs: "> -->
<!-- </TextBlock> -->
<!-- <TextBlock FontStyle="Italic" -->
<!-- FontSize="12" -->
<!-- Opacity="0.8" Text="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}" /> -->
<!-- </StackPanel> -->
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel Grid.Column="1" Orientation="Horizontal" VerticalAlignment="Center">
<StackPanel VerticalAlignment="Center" Margin="0 0 5 0"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).SonarrAvailable}">

View file

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel"
x:Class="CRD.Views.SettingsPageView"
@ -51,7 +52,8 @@
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList}"
SelectedItems="{Binding SelectedDubLang}">
SelectedItems="{Binding SelectedDubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
@ -101,7 +103,8 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="listBoxSubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList}" SelectedItems="{Binding SelectedSubLang}">
ItemsSource="{Binding SubLangList}" SelectedItems="{Binding SelectedSubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
@ -120,17 +123,77 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Include Signs Subtitles ">
<controls:SettingsExpanderItem Content="Signs Subtitles " Description="Download Signs (Forced) Subtitles">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeSignSubs}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeSignSubs}"> </CheckBox>
<controls:SettingsExpanderItem Content="Include CC Subtitles ">
<!-- <StackPanel> -->
<!-- -->
<!-- <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> -->
<!-- <TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 5 0" Text="Enabled"></TextBlock> -->
<!-- <CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeSignSubs}"> </CheckBox> -->
<!-- </StackPanel> -->
<!-- -->
<!-- <StackPanel Orientation="Horizontal" IsVisible="{Binding IncludeSignSubs}"> -->
<!-- <TextBlock VerticalAlignment="Center" Margin="0 0 5 0" Text="Mark as forced in mkv muxing"></TextBlock> -->
<!-- <CheckBox IsChecked="{Binding SignsSubsAsForced}"> </CheckBox> -->
<!-- </StackPanel> -->
<!-- -->
<!-- </StackPanel> -->
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding IncludeSignSubs}" Content="Signs Subtitles" Description="Mark as forced in mkv muxing">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeCcSubs}"> </CheckBox>
<CheckBox IsChecked="{Binding SignsSubsAsForced}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="CC Subtitles " Description="Download CC Subtitles">
<controls:SettingsExpanderItem.Footer>
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeCcSubs}"> </CheckBox>
<!-- <StackPanel> -->
<!-- <StackPanel Orientation="Horizontal" HorizontalAlignment="Right"> -->
<!-- <TextBlock VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 5 0" Text="Enabled"></TextBlock> -->
<!-- <CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeCcSubs}"> </CheckBox> -->
<!-- </StackPanel> -->
<!-- <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IncludeCcSubs}"> -->
<!-- <TextBlock VerticalAlignment="Center" Margin="0 0 5 0" Text="Mark as hearing impaired sub in mkv muxing"></TextBlock> -->
<!-- <CheckBox IsChecked="{Binding CCSubsMuxingFlag}"> </CheckBox> -->
<!-- </StackPanel> -->
<!-- -->
<!-- <StackPanel Orientation="Horizontal" HorizontalAlignment="Right" IsVisible="{Binding IncludeCcSubs}"> -->
<!-- <TextBlock VerticalAlignment="Center" Margin="0 0 5 0" Text="Font"></TextBlock> -->
<!-- <TextBox HorizontalAlignment="Left" MinWidth="250" -->
<!-- Text="{Binding CCSubsFont}" /> -->
<!-- </StackPanel> -->
<!-- </StackPanel> -->
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding IncludeCcSubs}" Content="CC Subtitles" Description="Mark as hearing impaired sub in mkv muxing">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding CCSubsMuxingFlag}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding IncludeCcSubs}" Content="CC Subtitles" Description="Font">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding CCSubsFont}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
@ -302,7 +365,7 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}">
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\">
<controls:SettingsExpanderItem.Footer>
<TextBox Name="FileNameTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileName}" />
@ -320,19 +383,25 @@
Description="MKVMerge and FFMpeg Settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="MP4" Description="Outputs a mp4 instead of an mkv - not recommended to use this option">
<controls:SettingsExpanderItem Content="Skip Muxing">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SkipMuxing}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</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.Footer>
<CheckBox IsChecked="{Binding MuxToMp4}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Keep Subtitles separate">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SkipSubMux}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Default Audio ">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Audio ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultDubLangList}"
@ -342,7 +411,7 @@
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Default Subtitle ">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Subtitle ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Vertical">
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
@ -355,27 +424,27 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Default Subtitle Signs" Description="Will set the signs subtitle as default instead">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Subtitle Signs" Description="Will set the signs subtitle as default instead">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DefaultSubSigns}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="File title"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width}">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="File title"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs}">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileTitle}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Include Episode description">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include Episode description">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeEpisodeDescription}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Episode description Language">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Episode description Language">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DescriptionLangList}"
@ -384,13 +453,13 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Sync Timings" Description="Does not work for all episodes but for the ones that only have a different intro">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Sync Timings" Description="Does not work for all episodes but for the ones that only have a different intro">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SyncTimings}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
@ -412,10 +481,10 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#4a4a4a" Background="#4a4a4a" BorderThickness="1"
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding ParamValue}" Margin="5,0" />
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:SettingsPageViewModel)DataContext).RemoveMkvMergeParam}"
@ -426,12 +495,10 @@
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Additional FFMpeg Options">
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Additional FFMpeg Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
@ -452,10 +519,10 @@
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#4a4a4a" Background="#4a4a4a" BorderThickness="1"
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding ParamValue}" Margin="5,0" />
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((vm:SettingsPageViewModel)DataContext).RemoveFfmpegParam}"
@ -469,6 +536,71 @@
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Encoding">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<CheckBox HorizontalAlignment="Right" Content="Enable Encoding?" IsChecked="{Binding IsEncodeEnabled}"> </CheckBox>
<ToggleButton x:Name="DropdownButtonEncodingPresets" IsVisible="{Binding IsEncodeEnabled}" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedEncodingPreset.stringValue}"
VerticalAlignment="Center" />
<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" />
</Grid>
</ToggleButton.Content>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
IsOpen="{Binding IsChecked, ElementName=DropdownButtonEncodingPresets, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonEncodingPresets}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxEncodingPresetSelection" SelectionMode="AlwaysSelected,Single" Width="210"
MaxHeight="400"
ItemsSource="{Binding EncodingPresetsList}"
SelectedItem="{Binding SelectedEncodingPreset}"
PointerWheelChanged="ListBox_PointerWheelChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
<StackPanel Orientation="Horizontal">
<Button HorizontalAlignment="Center" Margin="5 10" IsVisible="{Binding IsEncodeEnabled}" Command="{Binding CreateEncodingPresetButtonPress}" CommandParameter="false">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" Margin=" 0 0 5 0" />
<TextBlock VerticalAlignment="Center" Text="Create Preset"></TextBlock>
</StackPanel>
</Button>
<Button HorizontalAlignment="Center" Margin="5 10" IsVisible="{Binding IsEncodeEnabled}" Command="{Binding CreateEncodingPresetButtonPress}" CommandParameter="true">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Edit" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
@ -516,6 +648,36 @@
</controls:SettingsExpander>
<controls:SettingsExpander Header="Proxy Settings"
IconSource="Wifi3"
Description="Adjust proxy settings requires a restart to take effect"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Use Proxy">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding ProxyEnabled}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Host">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding ProxyHost}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Port">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="65535"
Value="{Binding ProxyPort}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="App Theme"
IconSource="DarkTheme"
Description="Change the current app theme">
@ -692,6 +854,27 @@
</controls:SettingsExpander>
<controls:SettingsExpander Header="IP"
IconSource="Wifi4"
Description="Check the current IP address to verify if traffic is being routed through a VPN">
<controls:SettingsExpander.Footer>
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border VerticalAlignment="Center" Height="30"> <!-- Match this to the Button's height -->
<TextBlock Text="{Binding CurrentIp}" VerticalAlignment="Center" FontSize="14" />
</Border>
<Button Grid.Column="1" Content="Check" Margin="10 0 0 0" Command="{Binding CheckIp}" />
</Grid>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<Grid Margin="0 0 0 10"
ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">

View file

@ -1,8 +1,10 @@
using Avalonia;
using System.Linq;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Sonarr;
@ -20,4 +22,19 @@ public partial class SettingsPageView : UserControl{
SonarrClient.Instance.RefreshSonarr();
}
}
private void ListBox_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){
var listBox = sender as ListBox;
var scrollViewer = listBox?.GetVisualDescendants().OfType<ScrollViewer>().FirstOrDefault();
if (scrollViewer != null){
// Determine if the ListBox is at its bounds (top or bottom)
bool atTop = scrollViewer.Offset.Y <= 0 && e.Delta.Y > 0;
bool atBottom = scrollViewer.Offset.Y >= scrollViewer.Extent.Height - scrollViewer.Viewport.Height && e.Delta.Y < 0;
if (atTop || atBottom){
e.Handled = true; // Stop the event from propagating to the parent
}
}
}
}

View file

@ -0,0 +1,109 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:utils="clr-namespace:CRD.ViewModels.Utils"
x:DataType="utils:ContentDialogEncodingPresetViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.Utils.ContentDialogEncodingPresetView">
<StackPanel Spacing="10" Margin="10">
<StackPanel IsVisible="{Binding EditMode}">
<TextBlock Text="Edit Preset" Margin="0,0,0,5" />
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding CustomPresetsList}"
SelectedItem="{Binding SelectedCustomPreset}">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding PresetName}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<!-- Preset Name -->
<StackPanel>
<TextBlock Text="Enter Preset Name" Margin="0,0,0,5" />
<TextBox Watermark="H.265 1080p" Text="{Binding PresetName}" />
<TextBlock Text="Preset name already used" FontSize="12" Foreground="{DynamicResource SystemFillColorCaution}"
IsVisible="{Binding FileExists}" />
</StackPanel>
<!-- Codec -->
<StackPanel>
<TextBlock Text="Enter Codec" Margin="0,10,0,5" />
<TextBox Watermark="libx265" Text="{Binding Codec}" />
</StackPanel>
<!-- Resolution ComboBox -->
<StackPanel>
<TextBlock Text="Select Resolution" Margin="0,10,0,5" />
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding ResolutionList}"
SelectedItem="{Binding SelectedResolution}">
</ComboBox>
</StackPanel>
<!-- Frame Rate NumberBox -->
<StackPanel>
<TextBlock Text="Enter Frame Rate" Margin="0,10,0,5" />
<controls:NumberBox Minimum="1" Maximum="999"
Value="{Binding FrameRate}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</StackPanel>
<!-- CRF NumberBox -->
<StackPanel>
<TextBlock Text="Enter CRF (0-51) - (cq,global_quality,qp)" Margin="0,10,0,5" />
<controls:NumberBox Minimum="0" Maximum="51"
Value="{Binding Crf}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</StackPanel>
<!-- Additional Parameters -->
<StackPanel Margin="0,20,0,0">
<TextBlock Text="Additional Parameters" Margin="0,0,0,5" />
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding AdditionalParametersString}" />
<Button HorizontalAlignment="Center" Command="{Binding AddAdditionalParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding AdditionalParameters}" Margin="0,10,0,0" MaxWidth="350">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="#4a4a4a" Background="{DynamicResource ControlAltFillColorQuarternary}" BorderThickness="1"
CornerRadius="10" Margin="2">
<StackPanel Orientation="Horizontal" Margin="5">
<TextBlock Text="{Binding stringValue}" Margin="5,0" />
<Button Content="X" FontSize="10" VerticalAlignment="Center"
HorizontalAlignment="Center" Width="15" Height="15" Padding="0"
Command="{Binding $parent[ItemsControl].((utils:ContentDialogEncodingPresetViewModel)DataContext).RemoveAdditionalParam}"
CommandParameter="{Binding .}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</UserControl>

View file

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

View file

@ -9,8 +9,7 @@
x:Class="CRD.Views.Utils.ContentDialogUpdateView">
<StackPanel Spacing="10" MinWidth="400">
<TextBlock Text="Please wait while the update is being downloaded..." HorizontalAlignment="Center" Margin="0,10,0,20"/>
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350"/>
</StackPanel>
</UserControl>