Add - Added path reset buttons to Temp Folder Path, Download Folder, and Background Image settings

Add - Added background image option to the Appearance settings
Add - Background Image Settings - Added new options to control opacity and blur radius
Add - Added "Couldn't sync dubs" status if the syncing failed
Add - Added functionality to combine multiple episodes from the same season into a single entry in the calendar
Add - Added video resolution display next to dubs/subs in the downloads tab
Add - Added Cloudflare check to image loading
Add - Added hardsub selection if the current is not available
Add - Added part size setting to configure the size of parts downloaded at the same time
Add - Added quality override to history series
Add - Added history marker to search results to indicate if a series is already in the user's history
Add - Added seasons tab for seasonal releases (Spring, Summer, Fall, Winter)
Add - Added potential releases and release times for the current day and the next week to the custom calendar
Chg - Changed Calendar cards background color for improved visibility
Chg - Combined Appearance settings into a single section in the settings tab
Chg - Consolidated Debug settings into one settings expander for better organization
Chg - Changed time sync to now check both the start and end of the video
Chg - Changed encoding progress to be displayed by the progress bar
Chg - Updated the functionality for hiding dubs in the custom calendar
Chg - Adjusted Dub sync to improve accuracy, resolving issues where it failed for more episodes than expected
Chg - Subtitles and dubs are now sorted according to the order selected in the MKV file
Chg - Changed logout behavior to correctly log out if login fails when starting the downloader
Chg - Changed that all downloaded files are removed if an in-progress download is removed from the queue
Chg - Changed default profile image
Chg - Updated used packages to the newest version
Chg - Separated settings to separate tabs
Fix - Fixed some series didn't get added to the history
Fix - Fixed an issue with file path length that prevented some files from being accessed properly
Fix - Fixed an issue where file names exceeded the maximum allowable length, causing errors
Fix - Fixed an issue where refreshing a series could get stuck
Fix - Fixed a crash that could happen with the syncing
Fix - Fixed an issue where the download status showed "Done" while moving files from the temp folder
Fix - Fixed an issue where cookies were not being utilized correctly
Fix - Resolved issues with displaying dates in UTC format
Fix - Fixed an issue with incorrect calendar grouping
Fix - Fixed an issue with the previous week navigation in the calendar
Fix - Fixed an issue where the calendar would not display correctly when not logged in
Fix - Fixed incorrect FFmpeg check for other OS (Linux/macOS)
Fix - Fixed an issue where image loading used a different HTTP client
This commit is contained in:
Elwador 2024-12-19 19:01:50 +01:00
parent 54224afeba
commit 95cd06a523
93 changed files with 5221 additions and 2657 deletions

View file

@ -1,5 +1,4 @@
using System; using System;
using System.IO;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@ -7,8 +6,6 @@ using CRD.ViewModels;
using MainWindow = CRD.Views.MainWindow; using MainWindow = CRD.Views.MainWindow;
using System.Linq; using System.Linq;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
namespace CRD; namespace CRD;

View file

@ -4,11 +4,16 @@ using System.Globalization;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData;
using HtmlAgilityPack; using HtmlAgilityPack;
using Newtonsoft.Json;
namespace CRD.Downloader; namespace CRD.Downloader;
@ -87,7 +92,7 @@ public class CalendarManager{
var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date"); var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date");
DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind); DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind);
if (week.FirstDayOfWeek == null){ if (week.FirstDayOfWeek == DateTime.MinValue){
week.FirstDayOfWeek = dayDateTime; week.FirstDayOfWeek = dayDateTime;
week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd"); week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd");
} }
@ -147,8 +152,10 @@ public class CalendarManager{
} }
public async Task<CalendarWeek> BuildCustomCalendar(bool forceUpdate){ public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
if (!forceUpdate && calendar.TryGetValue("C" + DateTime.Now.ToString("yyyy-MM-dd"), out var forDate)){ await LoadAnilistUpcoming();
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
return forDate; return forDate;
} }
@ -156,14 +163,14 @@ public class CalendarManager{
CalendarWeek week = new CalendarWeek(); CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>(); week.CalendarDays = new List<CalendarDay>();
DateTime today = DateTime.Now; DateTime targetDay = calTargetDate;
for (int i = 0; i < 7; i++){ for (int i = 0; i < 7; i++){
CalendarDay calDay = new CalendarDay(); CalendarDay calDay = new CalendarDay();
calDay.CalendarEpisodes = new List<CalendarEpisode>(); calDay.CalendarEpisodes = new List<CalendarEpisode>();
calDay.DateTime = today.AddDays(-i); calDay.DateTime = targetDay.AddDays(-i);
calDay.DayName = calDay.DateTime.Value.DayOfWeek.ToString(); calDay.DayName = calDay.DateTime.DayOfWeek.ToString();
week.CalendarDays.Add(calDay); week.CalendarDays.Add(calDay);
} }
@ -171,21 +178,68 @@ public class CalendarManager{
week.CalendarDays.Reverse(); week.CalendarDays.Reverse();
var firstDayOfWeek = week.CalendarDays.First().DateTime; var firstDayOfWeek = week.CalendarDays.First().DateTime;
week.FirstDayOfWeek = firstDayOfWeek;
var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes(CrunchyrollManager.Instance.CrunOptions.HistoryLang, 200, firstDayOfWeek, true); var newEpisodesBase = await CrunchyrollManager.Instance.CrEpisode.GetNewEpisodes("", 200, firstDayOfWeek, true);
if (newEpisodesBase is{ Data.Count: > 0 }){ if (newEpisodesBase is{ Data.Count: > 0 }){
var newEpisodes = newEpisodesBase.Data; var newEpisodes = newEpisodesBase.Data;
//EpisodeAirDate
foreach (var crBrowseEpisode in newEpisodes){ foreach (var crBrowseEpisode in newEpisodes){
var targetDate = CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate ? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate : crBrowseEpisode.LastPublic; DateTime episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.Kind == DateTimeKind.Utc
? crBrowseEpisode.EpisodeMetadata.EpisodeAirDate.ToLocalTime()
: crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && DateTime premiumAvailableStart = crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.Kind == DateTimeKind.Utc
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){ ? crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.ToLocalTime()
continue; : crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate;
DateTime now = DateTime.Now;
DateTime oneYearFromNow = now.AddYears(1);
DateTime targetDate;
if (CrunchyrollManager.Instance.CrunOptions.CalendarFilterByAirDate){
targetDate = episodeAirDate;
if (targetDate >= oneYearFromNow){
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
if (freeAvailableStart <= oneYearFromNow){
targetDate = freeAvailableStart;
} else{
targetDate = premiumAvailableStart;
}
}
} else{
targetDate = premiumAvailableStart;
if (targetDate >= oneYearFromNow){
DateTime freeAvailableStart = crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.Kind == DateTimeKind.Utc
? crBrowseEpisode.EpisodeMetadata.FreeAvailableDate.ToLocalTime()
: crBrowseEpisode.EpisodeMetadata.FreeAvailableDate;
if (freeAvailableStart <= oneYearFromNow){
targetDate = freeAvailableStart;
} else{
targetDate = episodeAirDate;
}
}
} }
var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter; var dubFilter = CrunchyrollManager.Instance.CrunOptions.CalendarDubFilter;
if (CrunchyrollManager.Instance.CrunOptions.CalendarHideDubs && crBrowseEpisode.EpisodeMetadata.SeasonTitle != null &&
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Audio)")) &&
(string.IsNullOrEmpty(dubFilter) || dubFilter == "none" || (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter))){
//|| crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp
continue;
}
if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){ if (!string.IsNullOrEmpty(dubFilter) && dubFilter != "none"){
if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){ if (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
continue; continue;
@ -193,7 +247,7 @@ public class CalendarManager{
} }
var calendarDay = (from day in week.CalendarDays var calendarDay = (from day in week.CalendarDays
where day.DateTime.HasValue && day.DateTime.Value.Date == targetDate.Date where day.DateTime != DateTime.MinValue && day.DateTime.Date == targetDate.Date
select day).FirstOrDefault(); select day).FirstOrDefault();
if (calendarDay != null){ if (calendarDay != null){
@ -209,13 +263,62 @@ public class CalendarManager{
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1"; calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle; calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode; calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
calEpisode.CrSeriesID = crBrowseEpisode.EpisodeMetadata.SeriesId;
calendarDay.CalendarEpisodes?.Add(calEpisode); var existingEpisode = calendarDay.CalendarEpisodes
?.FirstOrDefault(e => e.SeasonName == calEpisode.SeasonName);
if (existingEpisode != null){
if (!int.TryParse(existingEpisode.EpisodeNumber, out var num)){
existingEpisode.EpisodeNumber = "...";
} else{
var existingNumbers = existingEpisode.EpisodeNumber
.Split('-')
.Select(n => int.TryParse(n, out var num) ? num : 0)
.Where(n => n > 0)
.ToList();
if (int.TryParse(calEpisode.EpisodeNumber, out var newEpisodeNumber)){
existingNumbers.Add(newEpisodeNumber);
}
existingNumbers.Sort();
var lowest = existingNumbers.First();
var highest = existingNumbers.Last();
// Update the existing episode's number to the new range
existingEpisode.EpisodeNumber = lowest == highest
? lowest.ToString()
: $"{lowest}-{highest}";
if (lowest == 1){
existingEpisode.IsPremiere = true;
}
}
existingEpisode.CalendarEpisodes.Add(calEpisode);
} else{
calendarDay.CalendarEpisodes?.Add(calEpisode);
}
}
}
foreach (var calendarDay in week.CalendarDays){
if (calendarDay.DateTime.Date >= DateTime.Now.Date){
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(calendarDay.DateTime.ToString("yyyy-MM-dd"))){
var list = ProgramManager.Instance.AnilistUpcoming[calendarDay.DateTime.ToString("yyyy-MM-dd")];
foreach (var calendarEpisode in list.Where(calendarEpisode => calendarDay.DateTime.Date == calendarEpisode.DateTime.Date)
.Where(calendarEpisode => calendarDay.CalendarEpisodes.All(ele => ele.CrSeriesID != calendarEpisode.CrSeriesID))){
calendarDay.CalendarEpisodes.Add(calendarEpisode);
}
}
} }
} }
foreach (var weekCalendarDay in week.CalendarDays){ foreach (var weekCalendarDay in week.CalendarDays){
if (weekCalendarDay.CalendarEpisodes != null) if (weekCalendarDay.CalendarEpisodes.Count > 0)
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
.OrderBy(e => e.DateTime) .OrderBy(e => e.DateTime)
.ThenBy(e => e.SeasonName) .ThenBy(e => e.SeasonName)
@ -232,9 +335,232 @@ public class CalendarManager{
if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList(); if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
} }
calendar["C" + DateTime.Now.ToString("yyyy-MM-dd")] = week; calendar["C" + calTargetDate.ToString("yyyy-MM-dd")] = week;
return week; return week;
} }
private async Task LoadAnilistUpcoming(){
DateTime today = DateTime.Today;
string formattedDate = today.ToString("yyyy-MM-dd");
if (ProgramManager.Instance.AnilistUpcoming.ContainsKey(formattedDate)){
return;
}
DateTimeOffset todayMidnight = DateTimeOffset.Now.Date;
long todayMidnightUnix = todayMidnight.ToUnixTimeSeconds();
long sevenDaysLaterUnix = todayMidnight.AddDays(8).ToUnixTimeSeconds();
AniListResponseCalendar? aniListResponse = null;
int currentPage = 1; // Start from page 1
bool hasNextPage;
do{
var variables = new{
weekStart = todayMidnightUnix,
weekEnd = sevenDaysLaterUnix,
page = currentPage
};
var payload = new{
query,
variables
};
string jsonPayload = JsonConvert.SerializeObject(payload, Formatting.Indented);
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Anilist){
Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json")
};
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine("Anilist Request Failed for upcoming calendar episodes");
return;
}
AniListResponseCalendar currentResponse = Helpers.Deserialize<AniListResponseCalendar>(
response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings
) ?? new AniListResponseCalendar();
aniListResponse ??= currentResponse;
if (aniListResponse != currentResponse){
aniListResponse.Data?.Page?.AiringSchedules?.AddRange(currentResponse.Data?.Page?.AiringSchedules ??[]);
}
hasNextPage = currentResponse.Data?.Page?.PageInfo?.HasNextPage ?? false;
currentPage++;
} while (hasNextPage && currentPage < 20);
var list = aniListResponse.Data?.Page?.AiringSchedules ??[];
list = list.Where(ele => ele.Media?.ExternalLinks != null && ele.Media.ExternalLinks.Any(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
List<CalendarEpisode> calendarEpisodes =[];
foreach (var anilistEle in list){
var calEp = new CalendarEpisode();
calEp.DateTime = DateTimeOffset.FromUnixTimeSeconds(anilistEle.AiringAt).UtcDateTime.ToLocalTime();
calEp.HasPassed = false;
calEp.EpisodeName = anilistEle.Media?.Title.English;
calEp.SeriesUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/series/";
calEp.EpisodeUrl = $"https://www.crunchyroll.com/{CrunchyrollManager.Instance.CrunOptions.HistoryLang}/watch/";
calEp.ThumbnailUrl = anilistEle.Media?.CoverImage.ExtraLarge ?? ""; //https://www.crunchyroll.com/i/coming_soon_beta_thumb.jpg
calEp.IsPremiumOnly = true;
calEp.IsPremiere = anilistEle.Episode == 1;
calEp.SeasonName = anilistEle.Media?.Title.English;
calEp.EpisodeNumber = anilistEle.Episode.ToString();
calEp.AnilistEpisode = true;
var crunchyrollID = "";
if (anilistEle.Media?.ExternalLinks != null){
var url = anilistEle.Media.ExternalLinks.First(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url;
string pattern = @"series\/([^\/]+)";
Match match = Regex.Match(url, pattern);
if (match.Success){
crunchyrollID = match.Groups[1].Value;
calEp.CrSeriesID = crunchyrollID;
if (CrunchyrollManager.Instance.CrunOptions.History){
var historySeries = CrunchyrollManager.Instance.HistoryList.FirstOrDefault(item => item.SeriesId == crunchyrollID);
if (historySeries != null){
var oldestRelease = DateTime.MinValue;
foreach (var historySeriesSeason in historySeries.Seasons){
if (historySeriesSeason.EpisodesList.Any()){
var releaseDate = historySeriesSeason.EpisodesList.Last().EpisodeCrPremiumAirDate;
if (releaseDate.HasValue && oldestRelease < releaseDate.Value){
oldestRelease = releaseDate.Value;
}
}
}
if (oldestRelease != DateTime.MinValue){
calEp.DateTime = new DateTime(
calEp.DateTime.Year,
calEp.DateTime.Month,
calEp.DateTime.Day,
oldestRelease.Hour,
oldestRelease.Minute,
oldestRelease.Second,
calEp.DateTime.Kind
);
}
}
}
} else{
crunchyrollID = "";
}
}
calendarEpisodes.Add(calEp);
}
foreach (var calendarEpisode in calendarEpisodes){
var airDate = calendarEpisode.DateTime.ToString("yyyy-MM-dd");
if (!ProgramManager.Instance.AnilistUpcoming.TryGetValue(airDate, out var value)){
value = new List<CalendarEpisode>();
ProgramManager.Instance.AnilistUpcoming[airDate] = value;
}
value.Add(calendarEpisode);
}
}
#region Query
private string query = @"query ($weekStart: Int, $weekEnd: Int, $page: Int) {
Page(page: $page) {
pageInfo {
hasNextPage
total
}
airingSchedules(
airingAt_greater: $weekStart
airingAt_lesser: $weekEnd
) {
id
episode
airingAt
media {
id
idMal
title {
romaji
native
english
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
season
format
synonyms
episodes
description
bannerImage
isAdult
coverImage {
extraLarge
color
}
trailer {
id
site
thumbnail
}
externalLinks {
site
icon
color
url
}
relations {
edges {
relationType(version: 2)
node {
id
title {
romaji
native
english
}
siteUrl
}
}
}
}
}
}
}";
#endregion
} }

View file

@ -1,8 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
@ -17,40 +16,51 @@ public class CrAuth{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task AuthAnonymous(){ public async Task AuthAnonymous(){
string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{ var formData = new Dictionary<string, string>{
{ "grant_type", "client_id" }, { "grant_type", "client_id" },
{ "scope", "offline_access" } { "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
}; };
var requestContent = new FormUrlEncodedContent(formData);
requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
Content = requestContent Content = requestContent
}; };
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); foreach (var header in crunchyAuthHeaders){
request.Headers.Add(header.Key, header.Value);
}
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (response.IsOk){ if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{ } else{
Console.Error.WriteLine("Anonymous login failed"); Console.Error.WriteLine("Anonymous login failed");
} }
crunInstance.Profile = new CrProfile{ crunInstance.Profile = new CrProfile{
Username = "???", Username = "???",
Avatar = "003-cr-hime-excited.png", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP", PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE" PreferredContentSubtitleLanguage = "de-DE"
}; };
} }
private void JsonTokenToFileAndVariable(string content){ private void JsonTokenToFileAndVariable(string content, string deviceId){
crunInstance.Token = Helpers.Deserialize<CrToken>(content, crunInstance.SettingsJsonSerializerSettings); crunInstance.Token = Helpers.Deserialize<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
if (crunInstance.Token is{ expires_in: not null }){
if (crunInstance.Token != null && crunInstance.Token.expires_in != null){ crunInstance.Token.device_id = deviceId;
crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in); crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in);
CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken); CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken);
@ -58,25 +68,36 @@ public class CrAuth{
} }
public async Task Auth(AuthData data){ public async Task Auth(AuthData data){
var formData = new Dictionary<string, string>{ string uuid = Guid.NewGuid().ToString();
{ "username", data.Username },
{ "password", data.Password },
{ "grant_type", "password" },
{ "scope", "offline_access" }
};
var requestContent = new FormUrlEncodedContent(formData);
requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ var formData = new Dictionary<string, string>{
{ "username", data.Username }, // Replace with actual data
{ "password", data.Password }, // Replace with actual data
{ "grant_type", "password" },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
Content = requestContent Content = requestContent
}; };
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); foreach (var header in crunchyAuthHeaders){
request.Headers.Add(header.Key, header.Value);
}
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (response.IsOk){ if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{ } else{
if (response.ResponseContent.Contains("invalid_credentials")){ if (response.ResponseContent.Contains("invalid_credentials")){
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10)); MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10));
@ -99,7 +120,7 @@ public class CrAuth{
return; return;
} }
var request = HttpClientReq.CreateRequestMessage(Api.BetaProfile, HttpMethod.Get, true, true, null); var request = HttpClientReq.CreateRequestMessage(ApiUrls.BetaProfile, HttpMethod.Get, true, true, null);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -109,7 +130,7 @@ public class CrAuth{
if (profileTemp != null){ if (profileTemp != null){
crunInstance.Profile = profileTemp; crunInstance.Profile = profileTemp;
var requestSubs = HttpClientReq.CreateRequestMessage(Api.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null); var requestSubs = HttpClientReq.CreateRequestMessage(ApiUrls.Subscription + crunInstance.Token.account_id, HttpMethod.Get, true, false, null);
var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs); var responseSubs = await HttpClientReq.Instance.SendHttpRequest(requestSubs);
@ -152,35 +173,50 @@ public class CrAuth{
public async Task LoginWithToken(){ public async Task LoginWithToken(){
if (crunInstance.Token?.refresh_token == null){ if (crunInstance.Token?.refresh_token == null){
Console.Error.WriteLine("Missing Refresh Token"); Console.Error.WriteLine("Missing Refresh Token");
await AuthAnonymous();
return; return;
} }
string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{ var formData = new Dictionary<string, string>{
{ "refresh_token", crunInstance.Token.refresh_token }, { "refresh_token", crunInstance.Token.refresh_token },
{ "grant_type", "refresh_token" }, { "scope", "offline_access" },
{ "scope", "offline_access" } { "device_id", uuid },
{ "device_type", "Chrome on Windows" },
{ "grant_type", "refresh_token" }
}; };
var requestContent = new FormUrlEncodedContent(formData);
requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
Content = requestContent Content = requestContent
}; };
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); foreach (var header in crunchyAuthHeaders){
request.Headers.Add(header.Key, header.Value);
}
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (response.IsOk){ if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
if (crunInstance.Token?.refresh_token != null){
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
await GetProfile();
}
} else{ } else{
Console.Error.WriteLine("Token Auth Failed"); Console.Error.WriteLine("Token Auth Failed");
} await AuthAnonymous();
if (crunInstance.Token?.refresh_token != null){
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
await GetProfile();
} }
} }
@ -198,24 +234,37 @@ public class CrAuth{
return; return;
} }
var formData = new Dictionary<string, string>{ string uuid = Guid.NewGuid().ToString();
{ "refresh_token", crunInstance.Token?.refresh_token ?? string.Empty },
{ "grant_type", "refresh_token" },
{ "scope", "offline_access" }
};
var requestContent = new FormUrlEncodedContent(formData);
requestContent.Headers.ContentType = new MediaTypeHeaderValue("application/x-www-form-urlencoded");
var request = new HttpRequestMessage(HttpMethod.Post, Api.BetaAuth){ var formData = new Dictionary<string, string>{
{ "refresh_token", crunInstance.Token.refresh_token },
{ "grant_type", "refresh_token" },
{ "scope", "offline_access" },
{ "device_id", uuid },
{ "device_type", "Chrome on Windows" }
};
var requestContent = new FormUrlEncodedContent(formData);
var crunchyAuthHeaders = new Dictionary<string, string>{
{ "Authorization", ApiUrls.authBasicSwitch },
{ "User-Agent", ApiUrls.ChromeUserAgent }
};
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.BetaAuth){
Content = requestContent Content = requestContent
}; };
request.Headers.Authorization = new AuthenticationHeaderValue("Basic", Api.authBasicSwitch); foreach (var header in crunchyAuthHeaders){
request.Headers.Add(header.Key, header.Value);
}
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (response.IsOk){ if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent); JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{ } else{
Console.Error.WriteLine("Refresh Token Auth Failed"); Console.Error.WriteLine("Refresh Token Auth Failed");
} }

View file

@ -26,7 +26,7 @@ public class CrEpisode(){
} }
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/episodes/{id}", HttpMethod.Get, true, true, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/episodes/{id}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -181,6 +181,7 @@ public class CrEpisode(){
}; };
epMeta.AvailableSubs = item.SubtitleLocales; epMeta.AvailableSubs = item.SubtitleLocales;
epMeta.Description = item.Description; epMeta.Description = item.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){ if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
epMeta.SelectedDubs = dubLang epMeta.SelectedDubs = dubLang
@ -236,7 +237,7 @@ public class CrEpisode(){
query["sort_by"] = "newly_added"; query["sort_by"] = "newly_added";
query["type"] = "episode"; query["type"] = "episode";
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Web; using System.Web;
using CRD.Utils; using CRD.Utils;
@ -26,7 +25,7 @@ public class CrMovies{
} }
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/movies/{id}", HttpMethod.Get, true, true, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/movies/{id}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -81,6 +80,7 @@ public class CrMovies{
}; };
epMeta.AvailableSubs = new List<string>(); epMeta.AvailableSubs = new List<string>();
epMeta.Description = episodeP.Description; epMeta.Description = episodeP.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
return epMeta; return epMeta;
} }

View file

@ -23,8 +23,8 @@ public class CrMusic{
} }
public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){ public async Task<CrunchyMusicVideoList?> ParseArtistMusicVideosByIdAsync(string id, string crLocale, bool forcedLang = false){
var musicVideosTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/music_videos", crLocale, forcedLang); var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/concerts", crLocale, forcedLang); var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", crLocale, forcedLang);
await Task.WhenAll(musicVideosTask, concertsTask); await Task.WhenAll(musicVideosTask, concertsTask);
@ -42,7 +42,7 @@ public class CrMusic{
} }
private async Task<CrunchyMusicVideo?> ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){ private async Task<CrunchyMusicVideo?> ParseMediaByIdAsync(string id, string crLocale, bool forcedLang, string endpoint){
var mediaList = await FetchMediaListAsync($"{Api.Content}/{endpoint}/{id}", crLocale, forcedLang); var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang);
switch (mediaList.Total){ switch (mediaList.Total){
case < 1: case < 1:
@ -110,6 +110,7 @@ public class CrMusic{
epMeta.AvailableSubs = new List<string>(); epMeta.AvailableSubs = new List<string>();
epMeta.Description = episodeP.Description; epMeta.Description = episodeP.Description;
epMeta.Music = true; epMeta.Music = true;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
return epMeta; return epMeta;
} }

View file

@ -77,6 +77,7 @@ public class CrSeries(){
Time = 0, Time = 0,
DownloadSpeed = 0 DownloadSpeed = 0
}; };
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
epMeta.Description = item.Description; epMeta.Description = item.Description;
epMeta.AvailableSubs = item.SubtitleLocales; epMeta.AvailableSubs = item.SubtitleLocales;
if (episode.Langs.Count > 0){ if (episode.Langs.Count > 0){
@ -308,7 +309,7 @@ public class CrSeries(){
} }
} }
var showRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query); var showRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(showRequest); var response = await HttpClientReq.Instance.SendHttpRequest(showRequest);
@ -329,7 +330,7 @@ public class CrSeries(){
} }
} }
var episodeRequest = HttpClientReq.CreateRequestMessage($"{Api.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query); var episodeRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/seasons/{seasonID}/episodes", HttpMethod.Get, true, true, query);
var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest); var episodeRequestResponse = await HttpClientReq.Instance.SendHttpRequest(episodeRequest);
@ -391,7 +392,7 @@ public class CrSeries(){
} }
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}/seasons", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -422,7 +423,7 @@ public class CrSeries(){
} }
} }
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/series/{id}", HttpMethod.Get, true, true, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -457,7 +458,7 @@ public class CrSeries(){
query["n"] = "6"; query["n"] = "6";
query["type"] = "top_results"; query["type"] = "top_results";
var request = HttpClientReq.CreateRequestMessage($"{Api.Search}", HttpMethod.Get, true, false, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);
@ -468,6 +469,20 @@ public class CrSeries(){
CrSearchSeriesBase? series = Helpers.Deserialize<CrSearchSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings); CrSearchSeriesBase? series = Helpers.Deserialize<CrSearchSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
if (crunInstance.CrunOptions.History){
var historyIDs = new HashSet<string>(crunInstance.HistoryList.Select(item => item.SeriesId ?? ""));
if (series?.Data != null){
foreach (var crSearchSeries in series.Data){
if (crSearchSeries.Items != null){
foreach (var crBrowseSeries in crSearchSeries.Items.Where(crBrowseSeries => historyIDs.Contains(crBrowseSeries.Id ?? "unknownID"))){
crBrowseSeries.IsInHistory = true;
}
}
}
}
}
return series; return series;
} }
@ -488,7 +503,7 @@ public class CrSeries(){
query["n"] = "50"; query["n"] = "50";
query["sort_by"] = "alphabetical"; query["sort_by"] = "alphabetical";
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query); var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Browse}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request); var response = await HttpClientReq.Instance.SendHttpRequest(request);

View file

@ -5,12 +5,12 @@ using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Xml; using System.Xml;
using Avalonia.Media;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.DRM; using CRD.Utils.DRM;
using CRD.Utils.Ffmpeg_Encoding; using CRD.Utils.Ffmpeg_Encoding;
@ -21,7 +21,10 @@ using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils;
using CRD.Views; using CRD.Views;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using LanguageItem = CRD.Utils.Structs.LanguageItem; using LanguageItem = CRD.Utils.Structs.LanguageItem;
@ -116,11 +119,15 @@ public class CrunchyrollManager{
options.Theme = "System"; options.Theme = "System";
options.SelectedCalendarLanguage = "en-us"; options.SelectedCalendarLanguage = "en-us";
options.CalendarDubFilter = "none"; options.CalendarDubFilter = "none";
options.CustomCalendar = true;
options.DlVideoOnce = true; options.DlVideoOnce = true;
options.StreamEndpoint = "web/firefox"; options.StreamEndpoint = "web/firefox";
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes; options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
options.HistoryLang = DefaultLocale; options.HistoryLang = DefaultLocale;
options.BackgroundImageOpacity = 0.5;
options.BackgroundImageBlurRadius = 10;
options.History = true; options.History = true;
CfgManager.UpdateSettingsFromFile(options); CfgManager.UpdateSettingsFromFile(options);
@ -141,7 +148,7 @@ public class CrunchyrollManager{
Profile = new CrProfile{ Profile = new CrProfile{
Username = "???", Username = "???",
Avatar = "003-cr-hime-excited.png", Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP", PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE", PreferredContentSubtitleLanguage = "de-DE",
HasPremium = false, HasPremium = false,
@ -235,6 +242,8 @@ public class CrunchyrollManager{
} }
if (options.SkipMuxing == false){ if (options.SkipMuxing == false){
bool syncError = false;
data.DownloadProgress = new DownloadProgress(){ data.DownloadProgress = new DownloadProgress(){
IsDownloading = true, IsDownloading = true,
Percent = 100, Percent = 100,
@ -275,6 +284,10 @@ public class CrunchyrollManager{
if (result is{ merger: not null, isMuxed: true }){ if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger); mergers.Add(result.merger);
} }
if (result.syncError){
syncError = true;
}
} }
foreach (var merger in mergers){ foreach (var merger in mergers){
@ -291,7 +304,7 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName)); await Helpers.RunFFmpegWithPresetAsync(merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data);
} }
if (CrunOptions.DownloadToTempFolder){ if (CrunOptions.DownloadToTempFolder){
@ -319,6 +332,8 @@ public class CrunchyrollManager{
}, },
fileNameAndPath); fileNameAndPath);
syncError = result.syncError;
if (result is{ merger: not null, isMuxed: true }){ if (result is{ merger: not null, isMuxed: true }){
result.merger.CleanUp(); result.merger.CleanUp();
} }
@ -334,7 +349,7 @@ public class CrunchyrollManager{
QueueManager.Instance.Queue.Refresh(); QueueManager.Instance.Queue.Refresh();
await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName)); await Helpers.RunFFmpegWithPresetAsync(result.merger?.options.Output, FfmpegEncoding.GetPreset(CrunOptions.EncodingPresetName), data);
} }
if (CrunOptions.DownloadToTempFolder){ if (CrunOptions.DownloadToTempFolder){
@ -349,10 +364,10 @@ public class CrunchyrollManager{
Percent = 100, Percent = 100,
Time = 0, Time = 0,
DownloadSpeed = 0, DownloadSpeed = 0,
Doing = "Done" Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "")
}; };
if (CrunOptions.RemoveFinishedDownload){ if (CrunOptions.RemoveFinishedDownload && !syncError){
QueueManager.Instance.Queue.Remove(data); QueueManager.Instance.Queue.Remove(data);
} }
} else{ } else{
@ -402,7 +417,6 @@ public class CrunchyrollManager{
data.DownloadProgress = new DownloadProgress{ data.DownloadProgress = new DownloadProgress{
IsDownloading = true, IsDownloading = true,
Done = true,
Percent = 100, Percent = 100,
Time = 0, Time = 0,
DownloadSpeed = 0, DownloadSpeed = 0,
@ -471,7 +485,7 @@ public class CrunchyrollManager{
#endregion #endregion
private async Task<(Merger? merger, bool isMuxed)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename){ private async Task<(Merger? merger, bool isMuxed, bool syncError)> MuxStreams(List<DownloadedMedia> data, CrunchyMuxOptions options, string filename){
var muxToMp3 = false; var muxToMp3 = false;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){ if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -480,7 +494,7 @@ public class CrunchyrollManager{
muxToMp3 = true; muxToMp3 = true;
} else{ } else{
Console.WriteLine("Skip muxing since no videos are downloaded"); Console.WriteLine("Skip muxing since no videos are downloaded");
return (null, false); return (null, false, false);
} }
} }
@ -523,9 +537,9 @@ public class CrunchyrollManager{
OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), OnlyAudio = data.Where(a => a.Type == DownloadMediaType.Audio).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}", Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc, Signs = a.Signs, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(), { File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
KeepAllVideos = options.KeepAllVideos, KeepAllVideos = options.KeepAllVideos,
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList), // Assuming MakeFontsList is properly defined Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList),
Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(), Chapters = data.Where(a => a.Type == DownloadMediaType.Chapters).Select(a => new MergerInput{ Language = a.Lang, Path = a.Path ?? string.Empty }).ToList(),
VideoTitle = options.VideoTitle, VideoTitle = options.VideoTitle,
Options = new MuxOptions(){ Options = new MuxOptions(){
@ -549,7 +563,7 @@ public class CrunchyrollManager{
Console.Error.WriteLine("MKVmerge not found"); Console.Error.WriteLine("MKVmerge not found");
} }
bool isMuxed; bool isMuxed, syncError = false;
if (options.SyncTiming && CrunOptions.DlVideoOnce){ if (options.SyncTiming && CrunOptions.DlVideoOnce){
var basePath = merger.options.OnlyVid.First().Path; var basePath = merger.options.OnlyVid.First().Path;
@ -559,6 +573,12 @@ public class CrunchyrollManager{
foreach (var syncVideo in syncVideosList){ foreach (var syncVideo in syncVideosList){
if (!string.IsNullOrEmpty(syncVideo.Path)){ if (!string.IsNullOrEmpty(syncVideo.Path)){
var delay = await merger.ProcessVideo(basePath, syncVideo.Path); var delay = await merger.ProcessVideo(basePath, syncVideo.Path);
if (delay <= -100){
syncError = true;
continue;
}
var audio = merger.options.OnlyAudio.FirstOrDefault(audio => audio.Language.CrLocale == syncVideo.Lang.CrLocale); var audio = merger.options.OnlyAudio.FirstOrDefault(audio => audio.Language.CrLocale == syncVideo.Lang.CrLocale);
if (audio != null){ if (audio != null){
audio.Delay = (int)(delay * 1000); audio.Delay = (int)(delay * 1000);
@ -585,21 +605,10 @@ public class CrunchyrollManager{
isMuxed = true; isMuxed = true;
} }
return (merger, isMuxed); return (merger, isMuxed, syncError);
} }
private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){ private async Task<DownloadResponse> DownloadMediaList(CrunchyEpMeta data, CrDownloadOptions options){
// if (CmsToken?.Cms == null){
// Console.WriteLine("Missing CMS Token");
// MainWindow.Instance.ShowError("Missing CMS Token - are you signed in?");
// return new DownloadResponse{
// Data = new List<DownloadedMedia>(),
// Error = true,
// FileName = "./unknown",
// ErrorText = "Login problem"
// };
// }
if (Profile.Username == "???"){ if (Profile.Username == "???"){
MainWindow.Instance.ShowError("User Account not recognized - are you signed in?"); MainWindow.Instance.ShowError("User Account not recognized - are you signed in?");
return new DownloadResponse{ return new DownloadResponse{
@ -610,31 +619,55 @@ public class CrunchyrollManager{
}; };
} }
if (!File.Exists(CfgManager.PathFFMPEG)){ if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
Console.Error.WriteLine("Missing ffmpeg"); if (!File.Exists(CfgManager.PathFFMPEG)){
MainWindow.Instance.ShowError("FFmpeg not found"); Console.Error.WriteLine("Missing ffmpeg");
return new DownloadResponse{ MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}");
Data = new List<DownloadedMedia>(), return new DownloadResponse{
Error = true, Data = new List<DownloadedMedia>(),
FileName = "./unknown", Error = true,
ErrorText = "Missing ffmpeg" FileName = "./unknown",
}; ErrorText = "Missing ffmpeg"
} };
}
if (!File.Exists(CfgManager.PathMKVMERGE)){ if (!File.Exists(CfgManager.PathMKVMERGE)){
Console.Error.WriteLine("Missing Mkvmerge"); Console.Error.WriteLine("Missing Mkvmerge");
MainWindow.Instance.ShowError("Mkvmerge not found"); MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}");
return new DownloadResponse{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
FileName = "./unknown", FileName = "./unknown",
ErrorText = "Missing Mkvmerge" ErrorText = "Missing Mkvmerge"
}; };
}
} else{
if (!Helpers.IsInstalled("ffmpeg", "-version") && !File.Exists(Path.Combine(AppContext.BaseDirectory, "lib", "ffmpeg"))){
Console.Error.WriteLine("Ffmpeg is not installed or not in the system PATH.");
MainWindow.Instance.ShowError("Ffmpeg is not installed on the system or not found in the PATH.");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Ffmpeg is not installed"
};
}
if (!Helpers.IsInstalled("mkvmerge", "--version") && !File.Exists(Path.Combine(AppContext.BaseDirectory, "lib", "mkvmerge"))){
Console.Error.WriteLine("Mkvmerge is not installed or not in the system PATH.");
MainWindow.Instance.ShowError("Mkvmerge is not installed on the system or not found in the PATH.");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Mkvmerge is not installed"
};
}
} }
if (!_widevine.canDecrypt){ if (!_widevine.canDecrypt){
Console.Error.WriteLine("L3 key files missing"); Console.Error.WriteLine("L3 key files missing");
MainWindow.Instance.ShowError("Can't find CDM files in widevine folder "); MainWindow.Instance.ShowError("Can't find CDM files in the Widevine folder.\nFor more information, please check the FAQ section in the Wiki on the GitHub page.");
return new DownloadResponse{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -645,7 +678,7 @@ public class CrunchyrollManager{
if (!File.Exists(CfgManager.PathMP4Decrypt)){ if (!File.Exists(CfgManager.PathMP4Decrypt)){
Console.Error.WriteLine("mp4decrypt not found"); Console.Error.WriteLine("mp4decrypt not found");
MainWindow.Instance.ShowError("Can't find mp4decrypt in lib folder "); MainWindow.Instance.ShowError($"Can't find mp4decrypt in lib folder at: {CfgManager.PathMP4Decrypt}");
return new DownloadResponse{ return new DownloadResponse{
Data = new List<DownloadedMedia>(), Data = new List<DownloadedMedia>(),
Error = true, Error = true,
@ -864,14 +897,52 @@ public class CrunchyrollManager{
return true; return true;
}).ToList(); }).ToList();
} else{ } else{
dlFailed = true; if (hsLangs.Count > 0){
var dialog = new ContentDialog(){
Title = "Hardsub Select",
PrimaryButtonText = "Select",
CloseButtonText = "Close"
};
return new DownloadResponse{ var viewModel = new ContentDialogDropdownSelectViewModel(dialog,
Data = new List<DownloadedMedia>(), data.SeriesTitle + (!string.IsNullOrEmpty(data.Season)
Error = dlFailed, ? " - S" + data.Season + "E" + (data.EpisodeNumber != string.Empty ? data.EpisodeNumber : data.AbsolutEpisodeNumberE)
FileName = "./unknown", : "") + " - " +
ErrorText = "Hardsubs not available" data.EpisodeTitle, hsLangs);
}; dialog.Content = new ContentDialogDropdownSelectView(){
DataContext = viewModel
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary){
string selectedValue = viewModel.SelectedDropdownItem.stringValue;
if (hsLangs.IndexOf(selectedValue) > -1){
Console.WriteLine($"Selecting stream with {Languages.Locale2language(selectedValue).Language} hardsubs");
streams = streams.Where((s) => s.HardsubLang != "-" && s.HardsubLang == selectedValue).ToList();
data.Hslang = selectedValue;
}
} else{
dlFailed = true;
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "Hardsub not available"
};
}
} else{
dlFailed = true;
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "No Hardsubs available"
};
}
} }
} }
} else{ } else{
@ -986,12 +1057,12 @@ public class CrunchyrollManager{
int chosenVideoQuality; int chosenVideoQuality;
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){ if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
chosenVideoQuality = 1; chosenVideoQuality = 1;
} else if (options.QualityVideo == "best"){ } else if (data.VideoQuality == "best"){
chosenVideoQuality = videos.Count; chosenVideoQuality = videos.Count;
} else if (options.QualityVideo == "worst"){ } else if (data.VideoQuality == "worst"){
chosenVideoQuality = 1; chosenVideoQuality = 1;
} else{ } else{
var tempIndex = videos.FindIndex(a => a.quality.height + "" == options.QualityVideo); var tempIndex = videos.FindIndex(a => a.quality.height + "" == data.VideoQuality.Replace("p", ""));
if (tempIndex < 0){ if (tempIndex < 0){
chosenVideoQuality = videos.Count; chosenVideoQuality = videos.Count;
} else{ } else{
@ -1049,6 +1120,7 @@ public class CrunchyrollManager{
variables.Add(new Variable("height", chosenVideoSegments.quality.height, false)); variables.Add(new Variable("height", chosenVideoSegments.quality.height, false));
variables.Add(new Variable("width", chosenVideoSegments.quality.width, false)); variables.Add(new Variable("width", chosenVideoSegments.quality.width, false));
if (string.IsNullOrEmpty(data.Resolution)) data.Resolution = chosenVideoSegments.quality.height + "p";
LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang); LanguageItem? lang = Languages.languages.FirstOrDefault(a => a.Code == curStream.AudioLang);
if (lang == null){ if (lang == null){
@ -1070,7 +1142,37 @@ public class CrunchyrollManager{
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray()); fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string onlyFileName = Path.GetFileNameWithoutExtension(fileName);
int maxLength = 220;
if (onlyFileName.Length > maxLength){
Console.Error.WriteLine($"Filename too long {onlyFileName}");
if (options.FileName.Split("\\").Last().Contains("${title}") && onlyFileName.Length - (data.EpisodeTitle ?? string.Empty).Length < maxLength){
var titleVariable = variables.Find(e => e.Name == "title");
if (titleVariable != null){
int excessLength = (onlyFileName.Length - maxLength);
if (excessLength > 0 && ((string)titleVariable.ReplaceWith).Length > excessLength){
titleVariable.ReplaceWith = ((string)titleVariable.ReplaceWith).Substring(0, ((string)titleVariable.ReplaceWith).Length - excessLength);
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
onlyFileName = Path.GetFileNameWithoutExtension(fileName);
if (onlyFileName.Length > maxLength){
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
}
}
}
} else{
fileName = Helpers.LimitFileNameLength(fileName, maxLength);
}
Console.Error.WriteLine($"Filename changed to {Path.GetFileNameWithoutExtension(fileName)}");
}
//string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.Name), variables, options.Numbers, options.Override).ToArray());
string outFile = fileName + "." + (epMeta.Lang?.CrLocale ?? lang.Value.CrLocale);
string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override) string tempFile = Path.Combine(FileNameManager.ParseFileName($"temp-{(currentVersion.Guid != null ? currentVersion.Guid : currentMediaId)}", variables, options.Numbers, options.Override)
.ToArray()); .ToArray());
@ -1163,7 +1265,7 @@ public class CrunchyrollManager{
var json = JsonConvert.SerializeObject(reqBodyData); var json = JsonConvert.SerializeObject(reqBodyData);
var reqBody = new StringContent(json, Encoding.UTF8, "application/json"); var reqBody = new StringContent(json, Encoding.UTF8, "application/json");
var decRequest = HttpClientReq.CreateRequestMessage($"{Api.DRM}", HttpMethod.Post, false, false, null); var decRequest = HttpClientReq.CreateRequestMessage($"{ApiUrls.DRM}", HttpMethod.Post, false, false, null);
decRequest.Content = reqBody; decRequest.Content = reqBody;
var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest); var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest);
@ -1270,6 +1372,7 @@ public class CrunchyrollManager{
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
} else{ } else{
Console.WriteLine("No Video downloaded"); Console.WriteLine("No Video downloaded");
} }
@ -1335,6 +1438,7 @@ public class CrunchyrollManager{
Lang = lang.Value, Lang = lang.Value,
IsPrimary = isPrimary IsPrimary = isPrimary
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} else{ } else{
Console.WriteLine("No Audio downloaded"); Console.WriteLine("No Audio downloaded");
} }
@ -1351,6 +1455,7 @@ public class CrunchyrollManager{
IsPrimary = isPrimary IsPrimary = isPrimary
}; };
files.Add(videoDownloadMedia); files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
} }
if (audioDownloaded){ if (audioDownloaded){
@ -1360,6 +1465,7 @@ public class CrunchyrollManager{
Lang = lang.Value, Lang = lang.Value,
IsPrimary = isPrimary IsPrimary = isPrimary
}); });
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} }
} }
} else if (options.Novids){ } else if (options.Novids){
@ -1410,6 +1516,7 @@ public class CrunchyrollManager{
File.WriteAllText($"{tsFile}.txt", string.Join("\r\n", compiledChapters)); File.WriteAllText($"{tsFile}.txt", string.Join("\r\n", compiledChapters));
files.Add(new DownloadedMedia{ Path = $"{tsFile}.txt", Lang = lang, Type = DownloadMediaType.Chapters }); files.Add(new DownloadedMedia{ Path = $"{tsFile}.txt", Lang = lang, Type = DownloadMediaType.Chapters });
data.downloadedFiles.Add($"{tsFile}.txt");
} catch{ } catch{
Console.Error.WriteLine("Failed to write chapter file"); Console.Error.WriteLine("Failed to write chapter file");
} }
@ -1429,9 +1536,9 @@ public class CrunchyrollManager{
} else{ } else{
Console.WriteLine("Subtitles downloading skipped!"); Console.WriteLine("Subtitles downloading skipped!");
} }
} }
await Task.Delay(options.Waittime); // await Task.Delay(options.Waittime);
} }
} }
@ -1468,12 +1575,14 @@ public class CrunchyrollManager{
Type = DownloadMediaType.Description, Type = DownloadMediaType.Description,
Path = fullPath, Path = fullPath,
}); });
data.downloadedFiles.Add(fullPath);
} else{ } else{
if (files.All(e => e.Type != DownloadMediaType.Description)){ if (files.All(e => e.Type != DownloadMediaType.Description)){
files.Add(new DownloadedMedia{ files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description, Type = DownloadMediaType.Description,
Path = fullPath, Path = fullPath,
}); });
data.downloadedFiles.Add(fullPath);
} }
} }
@ -1649,6 +1758,7 @@ public class CrunchyrollManager{
Lang = sxData.Language, Lang = sxData.Language,
RelatedVideoDownloadMedia = videoDownloadMedia RelatedVideoDownloadMedia = videoDownloadMedia
}); });
data.downloadedFiles.Add(sxData.Path);
} else{ } else{
Console.WriteLine($"Failed to download subtitle: ${sxData.File}"); Console.WriteLine($"Failed to download subtitle: ${sxData.File}");
} }
@ -1678,7 +1788,10 @@ public class CrunchyrollManager{
M3U8Json videoJson = new M3U8Json{ M3U8Json videoJson = new M3U8Json{
Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList() Segments = chosenVideoSegments.segments.Cast<dynamic>().ToList()
}; };
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s");
data.downloadedFiles.Add(chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s.resume" : $"{tsFile}.video.m4s.resume");
var videoDownloader = new HlsDownloader(new HlsOptions{ var videoDownloader = new HlsDownloader(new HlsOptions{
Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s", Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s",
Timeout = options.Timeout, Timeout = options.Timeout,
@ -1689,6 +1802,7 @@ public class CrunchyrollManager{
Override = options.Force, Override = options.Force,
}, data, true, false); }, data, true, false);
var videoDownloadResult = await videoDownloader.Download(); var videoDownloadResult = await videoDownloader.Download();
return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile); return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile);
@ -1730,6 +1844,9 @@ public class CrunchyrollManager{
M3U8Json audioJson = new M3U8Json{ M3U8Json audioJson = new M3U8Json{
Segments = chosenAudioSegments.segments.Cast<dynamic>().ToList() Segments = chosenAudioSegments.segments.Cast<dynamic>().ToList()
}; };
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s");
data.downloadedFiles.Add(chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s.resume" : $"{tsFile}.audio.m4s.resume");
var audioDownloader = new HlsDownloader(new HlsOptions{ var audioDownloader = new HlsDownloader(new HlsOptions{
Output = chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s", Output = chosenAudioSegments.pssh != null ? $"{tempTsFile}.audio.enc.m4s" : $"{tsFile}.audio.m4s",
@ -1863,7 +1980,7 @@ public class CrunchyrollManager{
private async Task<bool> ParseChapters(string currentMediaId, List<string> compiledChapters){ private async Task<bool> ParseChapters(string currentMediaId, List<string> compiledChapters){
var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null); var showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/skip-events/production/{currentMediaId}.json", HttpMethod.Get, true, true, null);
var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest); var showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
if (showRequestResponse.IsOk){ if (showRequestResponse.IsOk){
CrunchyChapters chapterData = new CrunchyChapters(); CrunchyChapters chapterData = new CrunchyChapters();
@ -1873,7 +1990,7 @@ public class CrunchyrollManager{
JObject jObject = JObject.Parse(showRequestResponse.ResponseContent); JObject jObject = JObject.Parse(showRequestResponse.ResponseContent);
if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){ if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){
chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime?>(); chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime>();
} }
if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){ if (jObject.TryGetValue("mediaId", out JToken mediaIdToken)){
@ -1954,7 +2071,7 @@ public class CrunchyrollManager{
showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, true, null); showRequest = HttpClientReq.CreateRequestMessage($"https://static.crunchyroll.com/datalab-intro-v2/{currentMediaId}.json", HttpMethod.Get, true, true, null);
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest); showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
if (showRequestResponse.IsOk){ if (showRequestResponse.IsOk){
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings); CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
@ -1988,7 +2105,7 @@ public class CrunchyrollManager{
return true; return true;
} }
Console.Error.WriteLine("Old Chapter API request failed"); Console.Error.WriteLine("Chapter request failed");
return false; return false;
} }

View file

@ -0,0 +1,550 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.ViewModels;
using CRD.ViewModels.Utils;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
// ReSharper disable InconsistentNaming
namespace CRD.Downloader.Crunchyroll.ViewModels;
public partial class CrunchyrollSettingsViewModel : ViewModelBase{
[ObservableProperty]
private bool _downloadVideo = true;
[ObservableProperty]
private bool _downloadAudio = true;
[ObservableProperty]
private bool _downloadChapters = true;
[ObservableProperty]
private bool _addScaledBorderAndShadow;
[ObservableProperty]
private bool _includeSignSubs;
[ObservableProperty]
private bool _includeCcSubs;
[ObservableProperty]
private ComboBoxItem _selectedScaledBorderAndShadow;
public ObservableCollection<ComboBoxItem> ScaledBorderAndShadow{ get; } = new(){
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: yes" },
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" },
};
[ObservableProperty]
private bool _skipMuxing;
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _syncTimings;
[ObservableProperty]
private bool _defaultSubSigns;
[ObservableProperty]
private bool _defaultSubForcedDisplay;
[ObservableProperty]
private bool _includeEpisodeDescription;
[ObservableProperty]
private bool _downloadVideoForEveryDub;
[ObservableProperty]
private bool _keepDubsSeparate;
[ObservableProperty]
private bool _skipSubMux;
[ObservableProperty]
private double? _leadingNumbers;
[ObservableProperty]
private double? _partSize;
[ObservableProperty]
private string _fileName = "";
[ObservableProperty]
private string _fileTitle = "";
[ObservableProperty]
private ObservableCollection<StringItem> _mkvMergeOptions = new();
[ObservableProperty]
private string _mkvMergeOption = "";
[ObservableProperty]
private string _ffmpegOption = "";
[ObservableProperty]
private ObservableCollection<StringItem> _ffmpegOptions = new();
[ObservableProperty]
private string _selectedSubs = "all";
[ObservableProperty]
private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
[ObservableProperty]
private string _selectedDubs = "ja-JP";
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedDubLang = new();
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private ComboBoxItem _selectedDefaultDubLang;
[ObservableProperty]
private ComboBoxItem _selectedDefaultSubLang;
[ObservableProperty]
private ComboBoxItem? _selectedVideoQuality;
[ObservableProperty]
private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang = new();
[ObservableProperty]
private Color _listBoxColor;
public ObservableCollection<ComboBoxItem> VideoQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "1080" },
new ComboBoxItem(){ Content = "720" },
new ComboBoxItem(){ Content = "480" },
new ComboBoxItem(){ Content = "360" },
new ComboBoxItem(){ Content = "240" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "128kB/s" },
new ComboBoxItem(){ Content = "96kB/s" },
new ComboBoxItem(){ Content = "64kB/s" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> HardSubLangList{ get; } = new(){
new ComboBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> DescriptionLangList{ get; } = new(){
new ComboBoxItem(){ Content = "default" },
new ComboBoxItem(){ Content = "de-DE" },
new ComboBoxItem(){ Content = "en-US" },
new ComboBoxItem(){ Content = "es-419" },
new ComboBoxItem(){ Content = "es-ES" },
new ComboBoxItem(){ Content = "fr-FR" },
new ComboBoxItem(){ Content = "it-IT" },
new ComboBoxItem(){ Content = "pt-BR" },
new ComboBoxItem(){ Content = "pt-PT" },
new ComboBoxItem(){ Content = "ru-RU" },
new ComboBoxItem(){ Content = "hi-IN" },
new ComboBoxItem(){ Content = "ar-SA" },
};
public ObservableCollection<ListBoxItem> DubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = new(){
};
public ObservableCollection<ListBoxItem> SubLangList{ get; } = new(){
new ListBoxItem(){ Content = "all" },
new ListBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } = new(){
new ComboBoxItem(){ Content = "web/firefox" },
new ComboBoxItem(){ Content = "console/switch" },
new ComboBoxItem(){ Content = "console/ps4" },
new ComboBoxItem(){ Content = "console/ps5" },
new ComboBoxItem(){ Content = "console/xbox_one" },
new ComboBoxItem(){ Content = "web/edge" },
// new ComboBoxItem(){ Content = "web/safari" },
new ComboBoxItem(){ Content = "web/chrome" },
new ComboBoxItem(){ Content = "web/fallback" },
// new ComboBoxItem(){ Content = "ios/iphone" },
// new ComboBoxItem(){ Content = "ios/ipad" },
new ComboBoxItem(){ Content = "android/phone" },
new ComboBoxItem(){ Content = "tv/samsung" },
};
[ObservableProperty]
private bool _isEncodeEnabled;
[ObservableProperty]
private StringItem _selectedEncodingPreset;
public ObservableCollection<StringItem> EncodingPresetsList{ get; } = new();
[ObservableProperty]
private bool _cCSubsMuxingFlag;
[ObservableProperty]
private string _cCSubsFont;
[ObservableProperty]
private bool _signsSubsAsForced;
private bool settingsLoaded;
public CrunchyrollSettingsViewModel(){
foreach (var languageItem in Languages.languages){
HardSubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
SubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
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;
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];
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == Languages.Locale2language(options.Hslang).CrLocale) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0];
ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null;
SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0];
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem);
}
var dubLang = DubLangList.Where(a => options.DubLang.Contains(a.Content)).ToList();
SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
AddScaledBorderAndShadow = options.SubsAddScaledBorder is ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo or ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
SelectedScaledBorderAndShadow = GetScaledBorderAndShadowFromOptions(options);
CCSubsFont = options.CcSubsFont ?? "";
CCSubsMuxingFlag = options.CcSubsMuxingFlag;
SignsSubsAsForced = options.SignsSubsAsForced;
SkipMuxing = options.SkipMuxing;
IsEncodeEnabled = options.IsEncodeEnabled;
DefaultSubForcedDisplay = options.DefaultSubForcedDisplay;
DefaultSubSigns = options.DefaultSubSigns;
PartSize = options.Partsize;
IncludeEpisodeDescription = options.IncludeVideoDescription;
FileTitle = options.VideoTitle ?? "";
IncludeSignSubs = options.IncludeSignsSubs;
IncludeCcSubs = options.IncludeCcSubs;
DownloadVideo = !options.Novids;
DownloadAudio = !options.Noaudio;
DownloadVideoForEveryDub = !options.DlVideoOnce;
KeepDubsSeparate = options.KeepDubsSeperate;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
SyncTimings = options.SyncTiming;
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
SelectedAudioQuality = qualityAudio ?? AudioQualityList[0];
ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null;
SelectedVideoQuality = qualityVideo ?? VideoQualityList[0];
MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){
foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam });
}
}
FfmpegOptions.Clear();
if (options.FfmpegOptions != null){
foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam });
}
}
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
MkvMergeOptions.CollectionChanged += Changes;
FfmpegOptions.CollectionChanged += Changes;
settingsLoaded = true;
}
private void UpdateSettings(){
if (!settingsLoaded){
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.DefaultSubSigns = DefaultSubSigns;
CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay = DefaultSubForcedDisplay;
CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription;
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
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;
CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10);
CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
CrunchyrollManager.Instance.CrunOptions.Partsize = Math.Clamp((int)(PartSize ?? 0), 0, 10000);
CrunchyrollManager.Instance.CrunOptions.SubsAddScaledBorder = GetScaledBorderAndShadowSelection();
List<string> softSubs = new List<string>();
foreach (var listBoxItem in SelectedSubLang){
softSubs.Add(listBoxItem.Content + "");
}
CrunchyrollManager.Instance.CrunOptions.DlSubs = softSubs;
string descLang = SelectedDescriptionLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
List<string> dubLangs = new List<string>();
foreach (var listBoxItem in SelectedDubLang){
dubLangs.Add(listBoxItem.Content + "");
}
CrunchyrollManager.Instance.CrunOptions.DubLang = dubLangs;
CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams;
List<string> ffmpegParams = new List<string>();
foreach (var ffmpegParam in FfmpegOptions){
ffmpegParams.Add(ffmpegParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
CfgManager.WriteSettingsToFile();
}
private ScaledBorderAndShadowSelection GetScaledBorderAndShadowSelection(){
if (!AddScaledBorderAndShadow){
return ScaledBorderAndShadowSelection.DontAdd;
}
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: yes"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: no"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo;
}
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
private ComboBoxItem GetScaledBorderAndShadowFromOptions(CrDownloadOptions options){
switch (options.SubsAddScaledBorder){
case (ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes):
return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: yes") ?? ScaledBorderAndShadow[0];
case ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo:
return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: no") ?? ScaledBorderAndShadow[0];
default:
return ScaledBorderAndShadow[0];
}
}
[RelayCommand]
public void AddMkvMergeParam(){
MkvMergeOptions.Add(new StringItem(){ stringValue = MkvMergeOption });
MkvMergeOption = "";
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void RemoveMkvMergeParam(StringItem param){
MkvMergeOptions.Remove(param);
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void AddFfmpegParam(){
FfmpegOptions.Add(new StringItem(){ stringValue = FfmpegOption });
FfmpegOption = "";
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public void RemoveFfmpegParam(StringItem param){
FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions));
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
base.OnPropertyChanged(e);
if (e.PropertyName is nameof(SelectedDubs)
or nameof(SelectedSubs)
or nameof(ListBoxColor)){
return;
}
UpdateSettings();
if (e.PropertyName is nameof(History)){
if (CrunchyrollManager.Instance.CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
if (!string.IsNullOrEmpty(decompressedJson)){
CrunchyrollManager.Instance.HistoryList = Helpers.Deserialize<ObservableCollection<HistorySeries>>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ??
new ObservableCollection<HistorySeries>();
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init();
}
}
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
_ = SonarrClient.Instance.RefreshSonarrLite();
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
}
[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];
}
}
}

View file

@ -0,0 +1,533 @@
<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:vm="clr-namespace:CRD.Downloader.Crunchyroll.ViewModels"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:CrunchyrollSettingsViewModel"
x:Class="CRD.Downloader.Crunchyroll.Views.CrunchyrollSettingsView">
<Design.DataContext>
<vm:CrunchyrollSettingsViewModel />
</Design.DataContext>
<ScrollViewer Padding="20 20 20 0">
<StackPanel Spacing="8">
<controls:SettingsExpander Header="Dub language"
IconSource="Speaker2"
Description="Change the selected dub language (with multiple dubs some can be out of sync)">
<controls:SettingsExpander.Footer>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedDubs}"
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=DropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList}"
SelectedItems="{Binding SelectedDubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Hardsubs language"
IconSource="FontColorFilled"
Description="Change the selected hardsub language">
<controls:SettingsExpander.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding HardSubLangList}"
SelectedItem="{Binding SelectedHSLang}">
</ComboBox>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Softsubs language"
IconSource="FontColor"
Description="Change the selected softsubs language">
<controls:SettingsExpander.Footer>
<StackPanel>
<ToggleButton x:Name="dropdownButton" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSubs}"
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=dropdownButton, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=dropdownButton}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="listBoxSubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList}" SelectedItems="{Binding SelectedSubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
</StackPanel>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="Add ScaledBorderAndShadow ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<ComboBox HorizontalContentAlignment="Center" IsVisible="{Binding AddScaledBorderAndShadow}" Margin="5 0" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding ScaledBorderAndShadow}"
SelectedItem="{Binding SelectedScaledBorderAndShadow}">
</ComboBox>
<CheckBox IsChecked="{Binding AddScaledBorderAndShadow}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Signs Subtitles " Description="Download Signs (Forced) Subtitles">
<controls:SettingsExpanderItem.Footer>
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeSignSubs}"> </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 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 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>
<controls:SettingsExpander Header="Download Settings"
IconSource="Download"
Description="Adjust download settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Download Parts"
Description="How many parts of the stream are downloaded simultaneously">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="10000"
Value="{Binding PartSize}"
SpinButtonPlacementMode="Hidden"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Video for every dub">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<CheckBox IsChecked="{Binding DownloadVideoForEveryDub}"> </CheckBox>
<CheckBox IsVisible="{Binding DownloadVideoForEveryDub}" Content="Keep files separate" IsChecked="{Binding KeepDubsSeparate}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video Quality">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding VideoQualityList}"
SelectedItem="{Binding SelectedVideoQuality}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Audio">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadAudio}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Audio Quality">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding AudioQualityList}"
SelectedItem="{Binding SelectedAudioQuality}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Chapters">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadChapters}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Filename Settings"
IconSource="Edit"
Description="Change how the files are named"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Leading 0 for seasons and episodes">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="5"
Value="{Binding LeadingNumbers}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\">
<controls:SettingsExpanderItem.Footer>
<TextBox Name="FileNameTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileName}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Muxing Settings"
IconSource="Repair"
Description="MKVMerge and FFMpeg Settings"
IsExpanded="False">
<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 IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SkipSubMux}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Audio ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultDubLangList}"
SelectedItem="{Binding SelectedDefaultDubLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Subtitle ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Vertical">
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultSubLangList}"
SelectedItem="{Binding SelectedDefaultSubLang}">
</ComboBox>
<CheckBox Content="Forced Display" IsChecked="{Binding DefaultSubForcedDisplay}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<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 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 IsVisible="{Binding !SkipMuxing}" Content="Include Episode description">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeEpisodeDescription}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Episode description Language">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DescriptionLangList}"
SelectedItem="{Binding SelectedDescriptionLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<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 IsVisible="{Binding !SkipMuxing}" Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Name="TargetTextBox2" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding MkvMergeOption }">
</TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddMkvMergeParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding MkvMergeOptions}" Margin="0,5" 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].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveMkvMergeParam}"
CommandParameter="{Binding .}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Additional FFMpeg Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FfmpegOption }">
</TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddFfmpegParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding FfmpegOptions}" Margin="0,5" 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].((vm:CrunchyrollSettingsViewModel)DataContext).RemoveFfmpegParam}"
CommandParameter="{Binding .}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</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>
</controls:SettingsExpander>
</StackPanel>
</ScrollViewer>
</UserControl>

View file

@ -0,0 +1,26 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.VisualTree;
namespace CRD.Downloader.Crunchyroll.Views;
public partial class CrunchyrollSettingsView : UserControl{
public CrunchyrollSettingsView(){
InitializeComponent();
}
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

@ -19,20 +19,18 @@ namespace CRD.Downloader;
public class History(){ public class History(){
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance; private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task CRUpdateSeries(string seriesId, string? seasonId){ public async Task<bool> CRUpdateSeries(string seriesId, string? seasonId){
await crunInstance.CrAuth.RefreshToken(true); await crunInstance.CrAuth.RefreshToken(true);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true); CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
if (parsedSeries == null){ if (parsedSeries == null){
Console.Error.WriteLine("Parse Data Invalid - series is maybe only available with VPN or got deleted"); Console.Error.WriteLine("Parse Data Invalid - series is maybe only available with VPN or got deleted");
return; return false;
} }
if (parsedSeries.Data != null){ if (parsedSeries.Data != null){
foreach (var s in parsedSeries.Data){ foreach (var s in parsedSeries.Data){
if (!string.IsNullOrEmpty(seasonId) && s.Id != seasonId) continue;
var sId = s.Id; var sId = s.Id;
if (s.Versions is{ Count: > 0 }){ if (s.Versions is{ Count: > 0 }){
foreach (var sVersion in s.Versions.Where(sVersion => sVersion.Original == true)){ foreach (var sVersion in s.Versions.Where(sVersion => sVersion.Original == true)){
@ -44,8 +42,11 @@ public class History(){
} }
} }
if (!string.IsNullOrEmpty(seasonId) && sId != seasonId) continue;
var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true); var seasonData = await crunInstance.CrSeries.GetSeasonDataById(sId, string.IsNullOrEmpty(crunInstance.CrunOptions.HistoryLang) ? crunInstance.DefaultLocale : crunInstance.CrunOptions.HistoryLang, true);
if (seasonData.Data != null) await UpdateWithSeasonData(seasonData.Data); if (seasonData.Data is{ Count: > 0 }) await UpdateWithSeasonData(seasonData.Data);
} }
@ -55,8 +56,11 @@ public class History(){
MatchHistorySeriesWithSonarr(false); MatchHistorySeriesWithSonarr(false);
await MatchHistoryEpisodesWithSonarr(false, historySeries); await MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile(); CfgManager.UpdateHistoryFile();
return true;
} }
} }
return false;
} }
@ -124,10 +128,12 @@ public class History(){
return (null, downloadDirPath); return (null, downloadDirPath);
} }
public (HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId, string episodeId){ public (HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath, string videoQuality) GetHistoryEpisodeWithDubListAndDownloadDir(string? seriesId, string? seasonId,
string episodeId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
var downloadDirPath = ""; var downloadDirPath = "";
var videoQuality = "";
List<string> dublist =[]; List<string> dublist =[];
List<string> sublist =[]; List<string> sublist =[];
@ -145,6 +151,10 @@ public class History(){
downloadDirPath = historySeries.SeriesDownloadPath; downloadDirPath = historySeries.SeriesDownloadPath;
} }
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
videoQuality = historySeries.HistorySeriesVideoQualityOverride;
}
if (historySeason != null){ if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historySeason.HistorySeasonDubLangOverride.Count > 0){ if (historySeason.HistorySeasonDubLangOverride.Count > 0){
@ -159,13 +169,17 @@ public class History(){
downloadDirPath = historySeason.SeasonDownloadPath; downloadDirPath = historySeason.SeasonDownloadPath;
} }
if (!string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
videoQuality = historySeason.HistorySeasonVideoQualityOverride;
}
if (historyEpisode != null){ if (historyEpisode != null){
return (historyEpisode, dublist, sublist, downloadDirPath); return (historyEpisode, dublist, sublist, downloadDirPath, videoQuality);
} }
} }
} }
return (null, dublist, sublist, downloadDirPath); return (null, dublist, sublist, downloadDirPath, videoQuality);
} }
public List<string> GetDubList(string? seriesId, string? seasonId){ public List<string> GetDubList(string? seriesId, string? seasonId){
@ -187,10 +201,11 @@ public class History(){
return dublist; return dublist;
} }
public List<string> GetSubList(string? seriesId, string? seasonId){ public (List<string> sublist, string videoQuality) GetSubList(string? seriesId, string? seasonId){
var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId); var historySeries = crunInstance.HistoryList.FirstOrDefault(series => series.SeriesId == seriesId);
List<string> sublist =[]; List<string> sublist =[];
var videoQuality = "";
if (historySeries != null){ if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId); var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
@ -198,12 +213,20 @@ public class History(){
sublist = historySeries.HistorySeriesSoftSubsOverride; sublist = historySeries.HistorySeriesSoftSubsOverride;
} }
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
videoQuality = historySeries.HistorySeriesVideoQualityOverride;
}
if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){ if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){
sublist = historySeason.HistorySeasonSoftSubsOverride; sublist = historySeason.HistorySeasonSoftSubsOverride;
} }
if (historySeason != null && !string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
videoQuality = historySeason.HistorySeasonVideoQualityOverride;
}
} }
return sublist; return (sublist, videoQuality);
} }
@ -242,15 +265,14 @@ public class History(){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id); var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == crunchyEpisode.Id);
if (historyEpisode == null){ if (historyEpisode == null){
var langList = new List<string>(); var langList = new List<string>();
if (crunchyEpisode.Versions != null){ if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{ } else{
langList.Add(crunchyEpisode.AudioLocale); langList.Add(crunchyEpisode.AudioLocale);
} }
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode), EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description, EpisodeDescription = crunchyEpisode.Description,
@ -260,19 +282,19 @@ public class History(){
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList), HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList),
HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales), HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales),
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
}; };
historySeason.EpisodesList.Add(newHistoryEpisode); historySeason.EpisodesList.Add(newHistoryEpisode);
} else{ } else{
var langList = new List<string>(); var langList = new List<string>();
if (crunchyEpisode.Versions != null){ if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{ } else{
langList.Add(crunchyEpisode.AudioLocale); langList.Add(crunchyEpisode.AudioLocale);
} }
//Update existing episode //Update existing episode
historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode); historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode);
historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _); historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _);
@ -366,6 +388,7 @@ public class History(){
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales); historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales); historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
} }
return; return;
} }
@ -505,16 +528,16 @@ public class History(){
}; };
foreach (var crunchyEpisode in seasonData){ foreach (var crunchyEpisode in seasonData){
var langList = new List<string>(); var langList = new List<string>();
if (crunchyEpisode.Versions != null){ if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale)); langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{ } else{
langList.Add(crunchyEpisode.AudioLocale); langList.Add(crunchyEpisode.AudioLocale);
} }
Languages.SortListByLangList(langList); Languages.SortListByLangList(langList);
var newHistoryEpisode = new HistoryEpisode{ var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode), EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description, EpisodeDescription = crunchyEpisode.Description,
@ -524,6 +547,7 @@ public class History(){
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _), SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = langList, HistoryEpisodeAvailableDubLang = langList,
HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales, HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales,
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
}; };
newSeason.EpisodesList.Add(newHistoryEpisode); newSeason.EpisodesList.Add(newHistoryEpisode);
@ -531,7 +555,7 @@ public class History(){
return newSeason; return newSeason;
} }
public void MatchHistorySeriesWithSonarr(bool updateAll){ public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){ if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return; return;

View file

@ -1,14 +1,19 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Platform.Storage;
using Avalonia.Styling; using Avalonia.Styling;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Utils.Updater; using CRD.Utils.Updater;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
@ -50,12 +55,18 @@ public partial class ProgramManager : ObservableObject{
#endregion #endregion
public Dictionary<string, List<AnilistSeries>> AnilistSeasons = new();
public Dictionary<string, List<CalendarEpisode>> AnilistUpcoming = new();
private readonly FluentAvaloniaTheme? _faTheme; private readonly FluentAvaloniaTheme? _faTheme;
private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>(); private Queue<Func<Task>> taskQueue = new Queue<Func<Task>>();
private bool exitOnTaskFinish = false; private bool exitOnTaskFinish = false;
public IStorageProvider StorageProvider;
public ProgramManager(){ public ProgramManager(){
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme; _faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme;
@ -106,7 +117,7 @@ public partial class ProgramManager : ObservableObject{
private async void Init(){ private async void Init(){
CrunchyrollManager.Instance.InitOptions(); CrunchyrollManager.Instance.InitOptions();
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync(); UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){ if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
@ -125,6 +136,10 @@ public partial class ProgramManager : ObservableObject{
} }
} }
if (!string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath)){
Helpers.SetBackgroundImage(CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath, CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity,
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius);
}
await CrunchyrollManager.Instance.Init(); await CrunchyrollManager.Instance.Init();
@ -133,6 +148,7 @@ public partial class ProgramManager : ObservableObject{
await WorkOffArgsTasks(); await WorkOffArgsTasks();
} }
private async Task WorkOffArgsTasks(){ private async Task WorkOffArgsTasks(){
if (taskQueue.Count == 0){ if (taskQueue.Count == 0){
return; return;
@ -149,13 +165,11 @@ public partial class ProgramManager : ObservableObject{
Console.WriteLine("Exiting..."); Console.WriteLine("Exiting...");
IClassicDesktopStyleApplicationLifetime? lifetime = (IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime; IClassicDesktopStyleApplicationLifetime? lifetime = (IClassicDesktopStyleApplicationLifetime)Application.Current?.ApplicationLifetime;
if (lifetime != null){ if (lifetime != null){
lifetime.Shutdown(); lifetime.Shutdown();
} else{ } else{
Environment.Exit(0); Environment.Exit(0);
} }
} }
} }

View file

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.CustomList; using CRD.Utils.CustomList;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
@ -102,7 +100,7 @@ public class QueueManager{
var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory); var sList = await CrunchyrollManager.Instance.CrEpisode.EpisodeData((CrunchyEpisode)episodeL, updateHistory);
(HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath) historyEpisode = (null, [], [], ""); (HistoryEpisode? historyEpisode, List<string> dublist, List<string> sublist, string downloadDirPath,string videoQuality) historyEpisode = (null, [], [], "","");
if (CrunchyrollManager.Instance.CrunOptions.History){ if (CrunchyrollManager.Instance.CrunOptions.History){
var episode = sList.EpisodeAndLanguages.Items.First(); var episode = sList.EpisodeAndLanguages.Items.First();
@ -143,6 +141,8 @@ public class QueueManager{
} }
} }
selected.VideoQuality = !string.IsNullOrEmpty(historyEpisode.videoQuality) ? historyEpisode.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs; selected.DownloadSubs = historyEpisode.sublist.Count > 0 ? historyEpisode.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;
Queue.Add(selected); Queue.Add(selected);
@ -162,6 +162,12 @@ public class QueueManager{
} }
} else{ } else{
Console.WriteLine("Episode couldn't be added to Queue"); Console.WriteLine("Episode couldn't be added to Queue");
Console.Error.WriteLine("Episode couldn't be added to Queue - Available dubs/subs: ");
var languages = sList.EpisodeAndLanguages.Items.Select((a, index) =>
$"{(a.IsPremiumOnly ? "+ " : "")}{sList.EpisodeAndLanguages.Langs.ElementAtOrDefault(index).CrLocale ?? "Unknown"}").ToArray();
Console.Error.WriteLine($"{selected.SeasonTitle} - Season {selected.Season} - {selected.EpisodeTitle} dubs - [{string.Join(", ", languages)}] subs - [{string.Join(", ", selected.AvailableSubs ?? [])}]");
MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2)); MessageBus.Current.SendMessage(new ToastMessage($"Couldn't add episode to the queue with current dub settings", ToastType.Error, 2));
} }
} else{ } else{
@ -184,7 +190,7 @@ public class QueueManager{
} }
public void CrAddEpMetaToQueue(CrunchyEpMeta epMeta){ public void CrAddMusicMetaToQueue(CrunchyEpMeta epMeta){
Queue.Add(epMeta); Queue.Add(epMeta);
MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1)); MessageBus.Current.SendMessage(new ToastMessage($"Added episode to the queue", ToastType.Information, 1));
} }
@ -249,7 +255,9 @@ public class QueueManager{
} }
var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId); var subLangList = CrunchyrollManager.Instance.History.GetSubList(crunchyEpMeta.ShowId, crunchyEpMeta.SeasonId);
crunchyEpMeta.DownloadSubs = subLangList.Count > 0 ? subLangList : CrunchyrollManager.Instance.CrunOptions.DlSubs;
crunchyEpMeta.VideoQuality = !string.IsNullOrEmpty(subLangList.videoQuality) ? subLangList.videoQuality : CrunchyrollManager.Instance.CrunOptions.QualityVideo;
crunchyEpMeta.DownloadSubs = subLangList.sublist.Count > 0 ? subLangList.sublist : CrunchyrollManager.Instance.CrunOptions.DlSubs;
Queue.Add(crunchyEpMeta); Queue.Add(crunchyEpMeta);

View file

@ -2,9 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives"> xmlns:uip="using:FluentAvalonia.UI.Controls.Primitives">
<!-- <!--
NavView style in MainView for main app navigation NavView style in MainView for main app navigation
While you are free to copy this into your own apps While you are free to copy this into your own apps

View file

@ -4,7 +4,6 @@ using System.IO;
using System.Net.Http; using System.Net.Http;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CRD.Utils.DRM; namespace CRD.Utils.DRM;

View file

@ -73,6 +73,9 @@ public enum Locale{
[EnumMember(Value = "zh-TW")] [EnumMember(Value = "zh-TW")]
ZhTw, ZhTw,
[EnumMember(Value = "zh-HK")]
ZhHk,
[EnumMember(Value = "ca-ES")] [EnumMember(Value = "ca-ES")]
CaEs, CaEs,
@ -94,9 +97,6 @@ public enum Locale{
[EnumMember(Value = "te-IN")] [EnumMember(Value = "te-IN")]
TeIn, TeIn,
[EnumMember(Value = "id-ID")]
idID,
} }
public static class EnumExtensions{ public static class EnumExtensions{

View file

@ -3,13 +3,11 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Reflection; using System.Reflection;
using CRD.Downloader; using System.Runtime.InteropServices;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll; using CRD.Utils.Structs.Crunchyroll;
using Newtonsoft.Json; using Newtonsoft.Json;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel; using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization; using YamlDotNet.Serialization;
using YamlDotNet.Serialization.NamingConventions; using YamlDotNet.Serialization.NamingConventions;
@ -24,16 +22,22 @@ public class CfgManager{
public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json"); public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json");
public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json"); public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json");
public static readonly string PathFFMPEG = Path.Combine(WorkingDirectory, "lib", "ffmpeg.exe"); private static readonly string ExecutableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
public static readonly string PathMKVMERGE = Path.Combine(WorkingDirectory, "lib", "mkvmerge.exe");
public static readonly string PathMP4Decrypt = Path.Combine(WorkingDirectory, "lib", "mp4decrypt.exe"); public static readonly string PathFFMPEG = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(WorkingDirectory, "lib", "ffmpeg.exe") :
File.Exists(Path.Combine(WorkingDirectory, "lib", "ffmpeg")) ? Path.Combine(WorkingDirectory, "lib", "ffmpeg") : "ffmpeg";
public static readonly string PathMKVMERGE = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? Path.Combine(WorkingDirectory, "lib", "mkvmerge.exe") :
File.Exists(Path.Combine(WorkingDirectory, "lib", "mkvmerge")) ? Path.Combine(WorkingDirectory, "lib", "mkvmerge") : "mkvmerge";
public static readonly string PathMP4Decrypt = Path.Combine(WorkingDirectory, "lib", "mp4decrypt" + ExecutableExtension);
public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine"); public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine");
public static readonly string PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video"); 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 PathENCODING_PRESETS_DIR = Path.Combine(WorkingDirectory, "presets");
public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp"); public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp");
public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "video"); public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "fonts");
public static readonly string PathLogFile = Path.Combine(WorkingDirectory, "logfile.txt"); public static readonly string PathLogFile = Path.Combine(WorkingDirectory, "logfile.txt");
@ -242,7 +246,7 @@ public class CfgManager{
Console.Error.WriteLine($"An error occurred: {ex.Message}"); Console.Error.WriteLine($"An error occurred: {ex.Message}");
} }
} }
public static void WriteJsonToFile(string pathToFile, object obj){ public static void WriteJsonToFile(string pathToFile, object obj){
try{ try{
// Check if the directory exists; if not, create it. // Check if the directory exists; if not, create it.

View file

@ -1,13 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Parser.Utils; using CRD.Utils.Parser.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -118,11 +116,9 @@ public class HlsDownloader{
} }
if (_data.M3U8Json != null){ if (_data.M3U8Json != null){
List<dynamic> segments = _data.M3U8Json.Segments; List<dynamic> segments = _data.M3U8Json.Segments;
// map has init uri outside is none init uri // map has init uri outside is none init uri
// Download init part // Download init part
if (segments[0].map != null && _data.Offset == 0 && !_data.SkipInit){ if (segments[0].map != null && _data.Offset == 0 && !_data.SkipInit){
@ -249,7 +245,7 @@ public class HlsDownloader{
int downloadedSeg = Math.Min(dlOffset, totalSeg); int downloadedSeg = Math.Min(dlOffset, totalSeg);
_data.Parts.Completed = downloadedSeg + _data.Offset; // _data.Parts.Completed = downloadedSeg + _data.Offset; //
var dataLog = GetDownloadInfo(_data.DateStart, _data.Parts.Completed, totalSeg, _data.BytesDownloaded,_data.TotalBytes); var dataLog = GetDownloadInfo(_data.DateStart, _data.Parts.Completed, totalSeg, _data.BytesDownloaded, _data.TotalBytes);
_data.BytesDownloaded = 0; _data.BytesDownloaded = 0;
// Save resume data to file // Save resume data to file
@ -268,6 +264,17 @@ public class HlsDownloader{
}; };
if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){ if (!QueueManager.Instance.Queue.Contains(_currentEpMeta)){
if (!_currentEpMeta.DownloadProgress.Done){
foreach (var downloadItemDownloadedFile in _currentEpMeta.downloadedFiles){
try{
if (File.Exists(downloadItemDownloadedFile)){
File.Delete(downloadItemDownloadedFile);
}
} catch (Exception e){
}
}
}
return (Ok: false, _data.Parts); return (Ok: false, _data.Parts);
} }
@ -285,7 +292,7 @@ public class HlsDownloader{
return (Ok: true, _data.Parts); return (Ok: true, _data.Parts);
} }
public static Info GetDownloadInfo(long dateStartUnix, int partsDownloaded, int partsTotal, long downloadedBytes,long totalDownloadedBytes){ public static Info GetDownloadInfo(long dateStartUnix, int partsDownloaded, int partsTotal, long downloadedBytes, long totalDownloadedBytes){
// Convert Unix timestamp to DateTime // Convert Unix timestamp to DateTime
DateTime dateStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime; DateTime dateStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime;
double dateElapsed = (DateTime.UtcNow - dateStart).TotalMilliseconds; double dateElapsed = (DateTime.UtcNow - dateStart).TotalMilliseconds;
@ -293,15 +300,12 @@ public class HlsDownloader{
// Calculate percentage // Calculate percentage
int percentFixed = (int)((double)partsDownloaded / partsTotal * 100); int percentFixed = (int)((double)partsDownloaded / partsTotal * 100);
int percent = percentFixed < 100 ? percentFixed : (partsTotal == partsDownloaded ? 100 : 99); int percent = percentFixed < 100 ? percentFixed : (partsTotal == partsDownloaded ? 100 : 99);
// Calculate download speed (bytes per second)
double downloadSpeed = downloadedBytes / (dateElapsed / 1000); double downloadSpeed = downloadedBytes / (dateElapsed / 1000);
// Calculate remaining time estimate
// double remainingTime = dateElapsed * (partsTotal / (double)partsDownloaded - 1);
int partsLeft = partsTotal - partsDownloaded; int partsLeft = partsTotal - partsDownloaded;
double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed; double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed;
return new Info{ return new Info{
Percent = percent, Percent = percent,
Time = remainingTime, Time = remainingTime,
@ -586,32 +590,6 @@ public class Data{
public long TotalBytes{ get; set; } public long TotalBytes{ get; set; }
} }
public class ProgressData{
public int Total{ get; set; }
public int Cur{ get; set; }
// Considering the dual type in TypeScript (number|string), you might opt for string in C# to accommodate both numeric and text representations.
// Alternatively, you could use a custom setter to handle numeric inputs as strings, or define two separate properties if the usage context is clear.
public string? Percent{ get; set; }
public double Time{ get; set; } // Assuming this represents a duration or timestamp, you might consider TimeSpan or DateTime based on context.
public double DownloadSpeed{ get; set; }
public long Bytes{ get; set; }
}
public class DownloadInfo{
public string? Image{ get; set; }
public Parent? Parent{ get; set; }
public string? Title{ get; set; }
public LanguageItem? Language{ get; set; }
public string? FileName{ get; set; }
}
public class Parent{
public string? Title{ get; set; }
}
public class PartsData{ public class PartsData{
public int First{ get; set; } public int First{ get; set; }
public int Total{ get; set; } public int Total{ get; set; }

View file

@ -1,5 +1,4 @@
using CRD.Downloader; using CRD.Downloader.Crunchyroll;
using CRD.Downloader.Crunchyroll;
namespace CRD.Utils.HLS; namespace CRD.Utils.HLS;

View file

@ -1,20 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http; using System.Runtime.InteropServices;
using System.Runtime.Serialization; using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll; using CRD.Downloader;
using CRD.Utils.Ffmpeg_Encoding; using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.JsonConv; using CRD.Utils.JsonConv;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music; using Microsoft.Win32;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CRD.Utils; namespace CRD.Utils;
@ -28,8 +31,9 @@ public class Helpers{
return JsonConvert.DeserializeObject<T>(json, serializerSettings); return JsonConvert.DeserializeObject<T>(json, serializerSettings);
} catch (JsonException ex){ } catch (JsonException ex){
Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}"); Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}");
throw;
} }
return default;
} }
public static string ConvertTimeFormat(string time){ public static string ConvertTimeFormat(string time){
@ -67,6 +71,8 @@ public class Helpers{
} }
public static void EnsureDirectoriesExist(string path){ public static void EnsureDirectoriesExist(string path){
Console.WriteLine($"Check if path exists: {path}");
// Check if the path is absolute // Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path); bool isAbsolute = Path.IsPathRooted(path);
@ -82,13 +88,17 @@ public class Helpers{
string cumulativePath = isAbsolute ? Path.GetPathRoot(directoryPath) : Environment.CurrentDirectory; string cumulativePath = isAbsolute ? Path.GetPathRoot(directoryPath) : Environment.CurrentDirectory;
// Get all directory parts // Get all directory parts
string[] directories = directoryPath.Split(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar); string[] directories = directoryPath.Split(Path.DirectorySeparatorChar);
// Start the loop from the correct initial index // Start the loop from the correct initial index
int startIndex = isAbsolute && directories.Length > 0 && string.IsNullOrEmpty(directories[0]) ? 2 : 0; int startIndex = isAbsolute && directories.Length > 0 && string.IsNullOrEmpty(directories[0]) ? 1 : 0;
if (isAbsolute && cumulativePath == "/"){
cumulativePath = "/";
}
for (int i = startIndex; i < directories.Length; i++){ for (int i = startIndex; i < directories.Length; i++){
// Skip empty parts (which can occur with UNC paths) // Skip empty parts
if (string.IsNullOrEmpty(directories[i])){ if (string.IsNullOrEmpty(directories[i])){
continue; continue;
} }
@ -104,6 +114,7 @@ public class Helpers{
} }
} }
public static bool IsValidPath(string path){ public static bool IsValidPath(string path){
char[] invalidChars = Path.GetInvalidPathChars(); char[] invalidChars = Path.GetInvalidPathChars();
@ -290,7 +301,16 @@ public class Helpers{
} }
} }
public static async Task<(bool IsOk, int ErrorCode)> RunFFmpegWithPresetAsync(string inputFilePath, VideoPreset preset){ private static string GetQualityOption(VideoPreset preset){
return preset.Codec switch{
"h264_nvenc" or "hevc_nvenc" => $"-cq {preset.Crf}", // For NVENC
"h264_qsv" or "hevc_qsv" => $"-global_quality {preset.Crf}", // For Intel QSV
"h264_amf" or "hevc_amf" => $"-qp {preset.Crf}", // For AMD VCE
_ => $"-crf {preset.Crf}", // For software codecs like libx264/libx265
};
}
public static async Task<(bool IsOk, int ErrorCode)> RunFFmpegWithPresetAsync(string inputFilePath, VideoPreset preset, CrunchyEpMeta? data = null){
try{ try{
string outputExtension = Path.GetExtension(inputFilePath); string outputExtension = Path.GetExtension(inputFilePath);
string directory = Path.GetDirectoryName(inputFilePath); string directory = Path.GetDirectoryName(inputFilePath);
@ -298,18 +318,17 @@ public class Helpers{
string tempOutputFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}_output{outputExtension}"); string tempOutputFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}_output{outputExtension}");
string additionalParams = string.Join(" ", preset.AdditionalParameters); string additionalParams = string.Join(" ", preset.AdditionalParameters);
string qualityOption; string qualityOption = GetQualityOption(preset);
if (preset.Codec == "h264_nvenc" || preset.Codec == "hevc_nvenc"){
qualityOption = $"-cq {preset.Crf}"; // For NVENC TimeSpan? totalDuration = await GetMediaDurationAsync(CfgManager.PathFFMPEG, inputFilePath);
} else if (preset.Codec == "h264_qsv" || preset.Codec == "hevc_qsv"){ if (totalDuration == null){
qualityOption = $"-global_quality {preset.Crf}"; // For Intel QSV Console.Error.WriteLine("Unable to retrieve input file duration.");
} else if (preset.Codec == "h264_amf" || preset.Codec == "hevc_amf"){
qualityOption = $"-qp {preset.Crf}"; // For AMD VCE
} else{ } else{
qualityOption = $"-crf {preset.Crf}"; // For software codecs like libx264/libx265 Console.WriteLine($"Total Duration: {totalDuration}");
} }
string ffmpegCommand = $"-loglevel warning -i \"{inputFilePath}\" -c:v {preset.Codec} {qualityOption} -vf \"scale={preset.Resolution},fps={preset.FrameRate}\" {additionalParams} \"{tempOutputFilePath}\"";
string ffmpegCommand = $"-loglevel info -i \"{inputFilePath}\" -c:v {preset.Codec} {qualityOption} -vf \"scale={preset.Resolution},fps={preset.FrameRate}\" {additionalParams} \"{tempOutputFilePath}\"";
using (var process = new Process()){ using (var process = new Process()){
process.StartInfo.FileName = CfgManager.PathFFMPEG; process.StartInfo.FileName = CfgManager.PathFFMPEG;
process.StartInfo.Arguments = ffmpegCommand; process.StartInfo.Arguments = ffmpegCommand;
@ -327,6 +346,9 @@ public class Helpers{
process.ErrorDataReceived += (sender, e) => { process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){ if (!string.IsNullOrEmpty(e.Data)){
Console.Error.WriteLine($"{e.Data}"); Console.Error.WriteLine($"{e.Data}");
if (data != null && totalDuration != null){
ParseProgress(e.Data, totalDuration.Value, data);
}
} }
}; };
@ -358,6 +380,72 @@ public class Helpers{
} }
} }
private static void ParseProgress(string progressString, TimeSpan totalDuration, CrunchyEpMeta data){
try{
if (progressString.Contains("time=")){
var timeIndex = progressString.IndexOf("time=") + 5;
var timeString = progressString.Substring(timeIndex, 11);
if (TimeSpan.TryParse(timeString, out var currentTime)){
int progress = (int)(currentTime.TotalSeconds / totalDuration.TotalSeconds * 100);
Console.WriteLine($"Progress: {progress:F2}%");
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = progress,
Time = 0,
DownloadSpeed = 0,
Doing = "Encoding"
};
QueueManager.Instance.Queue.Refresh();
}
}
} catch (Exception e){
Console.Error.WriteLine("Failed to calculate encoding progess");
Console.Error.WriteLine(e.Message);
}
}
public static async Task<TimeSpan?> GetMediaDurationAsync(string ffmpegPath, string inputFilePath){
try{
using (var process = new Process()){
process.StartInfo.FileName = ffmpegPath;
process.StartInfo.Arguments = $"-i \"{inputFilePath}\"";
process.StartInfo.UseShellExecute = false;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.CreateNoWindow = true;
string output = string.Empty;
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
output += e.Data + Environment.NewLine;
}
};
process.Start();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
Regex regex = new Regex(@"Duration: (\d{2}):(\d{2}):(\d{2}\.\d{2})");
Match match = regex.Match(output);
if (match.Success){
int hours = int.Parse(match.Groups[1].Value);
int minutes = int.Parse(match.Groups[2].Value);
double seconds = double.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture);
return new TimeSpan(hours, minutes, (int)seconds);
}
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred while retrieving media duration: {ex.Message}");
}
return null;
}
public static double CalculateCosineSimilarity(string text1, string text2){ public static double CalculateCosineSimilarity(string text1, string text2){
var vector1 = ComputeWordFrequency(text1); var vector1 = ComputeWordFrequency(text1);
var vector2 = ComputeWordFrequency(text2); var vector2 = ComputeWordFrequency(text2);
@ -438,26 +526,28 @@ public class Helpers{
} }
public static async Task<Bitmap?> LoadImage(string imageUrl,int desiredWidth = 0,int desiredHeight = 0){ public static async Task<Bitmap?> LoadImage(string imageUrl, int desiredWidth = 0, int desiredHeight = 0){
try{ try{
using (var client = new HttpClient()){ var response = await HttpClientReq.Instance.GetHttpClient().GetAsync(imageUrl);
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
var bitmap = new Bitmap(stream);
if (desiredWidth != 0 && desiredHeight != 0){ if (ChallengeDetector.IsClearanceRequired(response)){
var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(desiredWidth, desiredHeight)); Console.Error.WriteLine($"Cloudflare Challenge detected ");
}
bitmap.Dispose(); response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
return scaledBitmap; var bitmap = new Bitmap(stream);
}
if (desiredWidth != 0 && desiredHeight != 0){
var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(desiredWidth, desiredHeight));
return bitmap;
bitmap.Dispose();
return scaledBitmap;
} }
return bitmap;
} }
} catch (Exception ex){ } catch (Exception ex){
Console.Error.WriteLine("Failed to load image: " + ex.Message); Console.Error.WriteLine("Failed to load image: " + ex.Message);
@ -513,4 +603,120 @@ public class Helpers{
string uuid = Guid.NewGuid().ToString(); string uuid = Guid.NewGuid().ToString();
return uuid; return uuid;
} }
public static string LimitFileNameLength(string fileName, int maxFileNameLength){
string directory = Path.GetDirectoryName(fileName) ?? string.Empty;
string name = Path.GetFileNameWithoutExtension(fileName);
string extension = Path.GetExtension(fileName);
if (name.Length > maxFileNameLength - extension.Length){
name = name.Substring(0, maxFileNameLength - extension.Length);
}
return Path.Combine(directory, name + extension);
}
public static string AddUncPrefixIfNeeded(string path){
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !IsLongPathEnabled()){
if (!string.IsNullOrEmpty(path) && !path.StartsWith(@"\\?\")){
return $@"\\?\{Path.GetFullPath(path)}";
}
}
return path;
}
private static bool IsLongPathEnabled(){
try{
using (var key = Registry.LocalMachine.OpenSubKey(@"SYSTEM\CurrentControlSet\Control\FileSystem")){
if (key != null){
var value = key.GetValue("LongPathsEnabled", 0);
return value is int intValue && intValue == 1;
}
}
} catch (Exception ex){
Console.Error.WriteLine($"Failed to check if long paths are enabled: {ex.Message}");
}
return false; // Default to false if unable to read the registry
}
private static Avalonia.Controls.Image? _backgroundImageLayer;
public static void SetBackgroundImage(string backgroundImagePath, double? imageOpacity = 0.5, double? blurRadius = 10){
try{
var activeWindow = GetActiveWindow();
if (activeWindow == null)
return;
if (activeWindow.Content is not Panel rootPanel){
rootPanel = new Grid();
activeWindow.Content = rootPanel;
}
if (string.IsNullOrEmpty(backgroundImagePath)){
if (_backgroundImageLayer != null){
rootPanel.Children.Remove(_backgroundImageLayer);
_backgroundImageLayer = null;
}
return;
}
if (_backgroundImageLayer == null){
_backgroundImageLayer = new Avalonia.Controls.Image{
Stretch = Stretch.UniformToFill,
ZIndex = -1,
};
rootPanel.Children.Add(_backgroundImageLayer);
}
_backgroundImageLayer.Source = new Bitmap(backgroundImagePath);
_backgroundImageLayer.Opacity = imageOpacity ?? 0.5;
_backgroundImageLayer.Effect = new BlurEffect{
Radius = blurRadius ?? 10
};
} catch (Exception ex){
Console.WriteLine($"Failed to set background image: {ex.Message}");
}
}
private static Window? GetActiveWindow(){
// Ensure the application is running with a Classic Desktop Lifetime
if (Application.Current?.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime){
// Return the first active window found in the desktop application's window list
return desktopLifetime.Windows.FirstOrDefault(window => window.IsActive);
}
return null;
}
public static bool IsInstalled(string checkFor, string versionString){
try{
// Create a new process for mkvmerge
Process process = new Process();
process.StartInfo.FileName = checkFor;
process.StartInfo.Arguments = versionString; // A harmless command to check if mkvmerge is available
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
// Start the process and wait for it to exit
process.Start();
process.WaitForExit();
// If the exit code is 0, mkvmerge was found and executed successfully
return process.ExitCode == 0;
} catch (Exception){
// If an exception is caught, mkvmerge is not installed or accessible
return false;
}
}
} }

View file

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.Http;
namespace CRD.Utils;
public class ChallengeDetector{
private static readonly HashSet<string> CloudflareServerNames = new HashSet<string>{
"cloudflare",
"cloudflare-nginx",
"ddos-guard"
};
/// <summary>
/// Checks if clearance is required.
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site requires clearance</returns>
public static bool IsClearanceRequired(HttpResponseMessage response) => IsCloudflareProtected(response);
/// <summary>
/// Checks if the site is protected by Cloudflare
/// </summary>
/// <param name="response">The HttpResponseMessage to check.</param>
/// <returns>True if the site is protected</returns>
private static bool IsCloudflareProtected(HttpResponseMessage response){
// check response headers
if (!response.Headers.Server.Any(i =>
i.Product != null && CloudflareServerNames.Contains(i.Product.Name.ToLower())))
return false;
// detect CloudFlare and DDoS-GUARD
if (response.StatusCode.Equals(HttpStatusCode.ServiceUnavailable) ||
response.StatusCode.Equals(HttpStatusCode.Forbidden)){
var responseHtml = response.Content.ReadAsStringAsync().Result;
if (responseHtml.Contains("<title>Just a moment...</title>") || // Cloudflare
responseHtml.Contains("<title>Access denied</title>") || // Cloudflare Blocked
responseHtml.Contains("<title>Attention Required! | Cloudflare</title>") || // Cloudflare Blocked
responseHtml.Trim().Equals("error code: 1020") || // Cloudflare Blocked
responseHtml.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1) // DDOS-GUARD
return true;
}
// detect Custom CloudFlare for EbookParadijs, Film-Paleis, MuziekFabriek and Puur-Hollands
if (response.Headers.Vary.ToString() == "Accept-Encoding,User-Agent" &&
response.Content.Headers.ContentEncoding.ToString() == "" &&
response.Content.ReadAsStringAsync().Result.ToLower().Contains("ddos"))
return true;
return false;
}
}

View file

@ -3,11 +3,11 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Linq;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Net.Sockets; using System.Net.Sockets;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
namespace CRD.Utils; namespace CRD.Utils;
@ -40,7 +40,6 @@ public class HttpClientReq{
private HttpClientHandler handler; private HttpClientHandler handler;
public HttpClientReq(){ public HttpClientReq(){
cookieStore = new Dictionary<string, CookieCollection>(); cookieStore = new Dictionary<string, CookieCollection>();
@ -64,7 +63,7 @@ public class HttpClientReq{
Console.Error.WriteLine("No proxy will be used."); Console.Error.WriteLine("No proxy will be used.");
handler = CreateHandler(false); handler = CreateHandler(false);
} }
client = new HttpClient(handler); client = new HttpClient(handler);
} else{ } else{
Console.Error.WriteLine("No proxy is being used."); Console.Error.WriteLine("No proxy is being used.");
@ -74,10 +73,19 @@ public class HttpClientReq{
Console.Error.WriteLine("No proxy is being used."); Console.Error.WriteLine("No proxy is being used.");
client = new HttpClient(CreateHttpClientHandler()); client = new HttpClient(CreateHttpClientHandler());
} }
client.Timeout = TimeSpan.FromSeconds(100);
// 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("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"); client.DefaultRequestHeaders.UserAgent.ParseAdd("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36");
// client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/1.9.0 Nintendo Switch/18.1.0.0 UE4/4.27");
// client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/3.60.0 Android/9 okhttp/4.12.0"); // client.DefaultRequestHeaders.UserAgent.ParseAdd("Crunchyroll/3.60.0 Android/9 okhttp/4.12.0");
client.DefaultRequestHeaders.Accept.ParseAdd("text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
client.DefaultRequestHeaders.AcceptEncoding.ParseAdd("gzip, deflate, br");
client.DefaultRequestHeaders.AcceptLanguage.ParseAdd("en-US,en;q=0.5");
client.DefaultRequestHeaders.Connection.ParseAdd("keep-alive");
} }
private HttpMessageHandler CreateHttpClientHandler(){ private HttpMessageHandler CreateHttpClientHandler(){
@ -132,8 +140,8 @@ public class HttpClientReq{
// handler.CookieContainer.Add(cookie); // handler.CookieContainer.Add(cookie);
// handler.CookieContainer.Add(cookie2); // handler.CookieContainer.Add(cookie2);
AddCookie("crunchyroll.com", new Cookie("etp_rt", refreshToken)); AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken));
AddCookie("crunchyroll.com", new Cookie("c_locale", "en-US")); AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"));
} }
private void AddCookie(string domain, Cookie cookie){ private void AddCookie(string domain, Cookie cookie){
@ -141,16 +149,26 @@ public class HttpClientReq{
cookieStore[domain] = new CookieCollection(); cookieStore[domain] = new CookieCollection();
} }
var existingCookie = cookieStore[domain].FirstOrDefault(c => c.Name == cookie.Name);
if (existingCookie != null){
cookieStore[domain].Remove(existingCookie);
}
cookieStore[domain].Add(cookie); cookieStore[domain].Add(cookie);
} }
public async Task<(bool IsOk, string ResponseContent)> SendHttpRequest(HttpRequestMessage request){ public async Task<(bool IsOk, string ResponseContent)> SendHttpRequest(HttpRequestMessage request, bool suppressError = false){
string content = string.Empty; string content = string.Empty;
try{ try{
AttachCookies(request); AttachCookies(request);
HttpResponseMessage response = await client.SendAsync(request); HttpResponseMessage response = await client.SendAsync(request);
if (ChallengeDetector.IsClearanceRequired(response)){
Console.Error.WriteLine($" Cloudflare Challenge detected");
}
content = await response.Content.ReadAsStringAsync(); content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
@ -158,28 +176,40 @@ public class HttpClientReq{
return (IsOk: true, ResponseContent: content); return (IsOk: true, ResponseContent: content);
} catch (Exception e){ } catch (Exception e){
// Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}"); // Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
Console.Error.WriteLine($"Error: {e} \n Response: {content}"); if (!suppressError){
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
}
return (IsOk: false, ResponseContent: content); return (IsOk: false, ResponseContent: content);
} }
} }
private void AttachCookies(HttpRequestMessage request){ private void AttachCookies(HttpRequestMessage request){
if (cookieStore.TryGetValue(request.RequestUri.Host, out CookieCollection cookies)){ var cookieHeader = new StringBuilder();
var cookieHeader = new StringBuilder();
foreach (Cookie cookie in cookies){ if (request.Headers.TryGetValues("Cookie", out var existingCookies)){
cookieHeader.Append(string.Join("; ", existingCookies));
}
foreach (var cookie in cookieStore.SelectMany(keyValuePair => keyValuePair.Value)){
string cookieString = $"{cookie.Name}={cookie.Value}";
if (!cookieHeader.ToString().Contains(cookieString)){
if (cookieHeader.Length > 0){ if (cookieHeader.Length > 0){
cookieHeader.Append("; "); cookieHeader.Append("; ");
} }
cookieHeader.Append($"{cookie.Name}={cookie.Value}"); cookieHeader.Append(cookieString);
}
if (cookieHeader.Length > 0){
request.Headers.Add("Cookie", cookieHeader.ToString());
} }
} }
if (cookieHeader.Length > 0){
request.Headers.Remove("Cookie");
request.Headers.Add("Cookie", cookieHeader.ToString());
}
} }
public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, bool disableDrmHeader, NameValueCollection? query){ public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, bool disableDrmHeader, NameValueCollection? query){
UriBuilder uriBuilder = new UriBuilder(uri); UriBuilder uriBuilder = new UriBuilder(uri);
@ -210,10 +240,12 @@ public class HttpClientReq{
} }
} }
public static class Api{ public static class ApiUrls{
public static readonly string ApiBeta = "https://beta-api.crunchyroll.com"; public static readonly string ApiBeta = "https://beta-api.crunchyroll.com";
public static readonly string ApiN = "https://www.crunchyroll.com"; public static readonly string ApiN = "https://www.crunchyroll.com";
public static readonly string Anilist = "https://graphql.anilist.co";
public static readonly string Auth = ApiN + "/auth/v1/token";
public static readonly string BetaAuth = ApiBeta + "/auth/v1/token"; public static readonly string BetaAuth = ApiBeta + "/auth/v1/token";
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile"; public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile";
public static readonly string BetaCmsToken = ApiBeta + "/index/v2"; public static readonly string BetaCmsToken = ApiBeta + "/index/v2";
@ -228,8 +260,10 @@ public static class Api{
public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/"; public static readonly string Subscription = ApiBeta + "/subs/v3/subscriptions/";
public static readonly string CmsN = ApiN + "/content/v2/cms"; public static readonly string CmsN = ApiN + "/content/v2/cms";
public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6";
public static readonly string authBasic = "bm9haWhkZXZtXzZpeWcwYThsMHE6"; public static readonly string authBasicMob = "Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=";
public static readonly string authBasicMob = "bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg="; public static readonly string authBasicSwitch = "Basic dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=";
public static readonly string authBasicSwitch = "dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=";
public static readonly string ChromeUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/129.0.0.0 Safari/537.36";
} }

View file

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Utils.Structs; using CRD.Utils.Structs;
namespace CRD.Utils.Muxing; namespace CRD.Utils.Muxing;
@ -54,6 +55,53 @@ public class FontsManager{
public string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/"; public string root = "https://static.crunchyroll.com/vilos-v2/web/vilos/assets/libass-fonts/";
public async Task GetFontsAsync(){
Console.WriteLine("Downloading fonts...");
var fonts = Fonts.Values.SelectMany(f => f).ToList();
foreach (var font in fonts){
var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font);
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){
Console.WriteLine($"{font} already downloaded!");
} else{
var fontFolder = Path.GetDirectoryName(fontLoc);
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0){
File.Delete(fontLoc);
}
try{
if (!Directory.Exists(fontFolder)){
Directory.CreateDirectory(fontFolder);
}
} catch (Exception e){
Console.WriteLine($"Failed to create directory: {e.Message}");
}
var fontUrl = root + font;
using (var httpClient = HttpClientReq.Instance.GetHttpClient()){
try{
var response = await httpClient.GetAsync(fontUrl);
if (response.IsSuccessStatusCode){
var fontData = await response.Content.ReadAsByteArrayAsync();
await File.WriteAllBytesAsync(fontLoc, fontData);
Console.WriteLine($"Downloaded: {font}");
} else{
Console.Error.WriteLine($"Failed to download: {font}");
}
} catch (Exception e){
Console.Error.WriteLine($"Error downloading {font}: {e.Message}");
}
}
}
}
Console.WriteLine("All required fonts downloaded!");
}
public static List<string> ExtractFontsFromAss(string ass){ public static List<string> ExtractFontsFromAss(string ass){
var lines = ass.Replace("\r", "").Split('\n'); var lines = ass.Replace("\r", "").Split('\n');
var styles = new List<string>(); var styles = new List<string>();
@ -76,7 +124,6 @@ public class FontsManager{
} }
public Dictionary<string, List<string>> GetDictFromKeyList(List<string> keysList){ public Dictionary<string, List<string>> GetDictFromKeyList(List<string> keysList){
Dictionary<string, List<string>> filteredDictionary = new Dictionary<string, List<string>>(); Dictionary<string, List<string>> filteredDictionary = new Dictionary<string, List<string>>();
foreach (string key in keysList){ foreach (string key in keysList){
@ -86,9 +133,8 @@ public class FontsManager{
} }
return filteredDictionary; return filteredDictionary;
} }
public static string GetFontMimeType(string fontFile){ public static string GetFontMimeType(string fontFile){
if (Regex.IsMatch(fontFile, @"\.otf$")) if (Regex.IsMatch(fontFile, @"\.otf$"))
@ -99,7 +145,7 @@ public class FontsManager{
return "application/octet-stream"; return "application/octet-stream";
} }
public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){ public List<ParsedFont> MakeFontsList(string fontsDir, List<SubtitleFonts> subs){
Dictionary<string, List<string>> fontsNameList = new Dictionary<string, List<string>>(); Dictionary<string, List<string>> fontsNameList = new Dictionary<string, List<string>>();
List<string> subsList = new List<string>(); List<string> subsList = new List<string>();
List<ParsedFont> fontsList = new List<ParsedFont>(); List<ParsedFont> fontsList = new List<ParsedFont>();
@ -108,12 +154,13 @@ public class FontsManager{
foreach (var s in subs){ foreach (var s in subs){
foreach (var keyValuePair in s.Fonts){ foreach (var keyValuePair in s.Fonts){
if (!fontsNameList.ContainsKey(keyValuePair.Key)){ if (!fontsNameList.ContainsKey(keyValuePair.Key)){
fontsNameList.Add(keyValuePair.Key,keyValuePair.Value); fontsNameList.Add(keyValuePair.Key, keyValuePair.Value);
} }
} }
subsList.Add(s.Language.Locale); subsList.Add(s.Language.Locale);
} }
if (subsList.Count > 0){ if (subsList.Count > 0){
Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsList), subsList.Count); Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsList), subsList.Count);
isNstr = false; isNstr = false;

View file

@ -1,6 +1,5 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization; using System.Globalization;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
@ -9,7 +8,6 @@ using System.Threading.Tasks;
using System.Xml; using System.Xml;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using DynamicData;
namespace CRD.Utils.Muxing; namespace CRD.Utils.Muxing;
@ -37,7 +35,7 @@ public class Merger{
var hasVideo = false; var hasVideo = false;
args.Add("-loglevel warning"); args.Add("-loglevel warning");
if (!options.mp3){ if (!options.mp3){
foreach (var vid in options.OnlyVid){ foreach (var vid in options.OnlyVid){
if (!hasVideo || options.KeepAllVideos == true){ if (!hasVideo || options.KeepAllVideos == true){
@ -76,6 +74,8 @@ public class Merger{
index++; index++;
} }
bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){ foreach (var sub in options.Subtitles.Select((value, i) => new{ value, i })){
if (sub.value.Delay != null && sub.value.Delay != 0){ if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0; double delay = sub.value.Delay / 1000.0 ?? 0;
@ -84,7 +84,9 @@ public class Merger{
args.Add($"-i \"{sub.value.File}\""); args.Add($"-i \"{sub.value.File}\"");
metaData.Add($"-map {index}:s"); metaData.Add($"-map {index}:s");
if (options.Defaults.Sub.Code == sub.value.Language.Code && CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == sub.value.Signs && sub.value.ClosedCaption == false){ if (options.Defaults.Sub.Code == sub.value.Language.Code &&
(CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == sub.value.Signs || CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns && !hasSignsSub)
&& sub.value.ClosedCaption == false){
metaData.Add($"-disposition:s:{sub.i} default"); metaData.Add($"-disposition:s:{sub.i} default");
} else{ } else{
metaData.Add($"-disposition:s:{sub.i} 0"); metaData.Add($"-disposition:s:{sub.i} 0");
@ -146,7 +148,7 @@ public class Merger{
bool hasVideo = false; bool hasVideo = false;
args.Add($"-o \"{options.Output}\""); args.Add($"-o \"{Helpers.AddUncPrefixIfNeeded(options.Output)}\"");
if (options.Options.mkvmerge != null){ if (options.Options.mkvmerge != null){
args.AddRange(options.Options.mkvmerge); args.AddRange(options.Options.mkvmerge);
} }
@ -162,11 +164,15 @@ public class Merger{
args.Add($"--language 0:{vid.Language.Code}"); args.Add($"--language 0:{vid.Language.Code}");
hasVideo = true; hasVideo = true;
args.Add($"\"{vid.Path}\""); args.Add($"\"{Helpers.AddUncPrefixIfNeeded(vid.Path)}\"");
} }
} }
var sortedAudio = options.OnlyAudio
.OrderBy(sub => CrunchyrollManager.Instance.CrunOptions.DubLang.IndexOf(sub.Language.CrLocale) != -1 ? CrunchyrollManager.Instance.CrunOptions.DubLang.IndexOf(sub.Language.CrLocale) : int.MaxValue)
.ToList();
foreach (var aud in options.OnlyAudio){ foreach (var aud in sortedAudio){
string trackName = aud.Language.Name; string trackName = aud.Language.Name;
args.Add("--audio-tracks 0"); args.Add("--audio-tracks 0");
args.Add("--no-video"); args.Add("--no-video");
@ -184,11 +190,19 @@ public class Merger{
args.Add($"--sync 0:{aud.Delay}"); args.Add($"--sync 0:{aud.Delay}");
} }
args.Add($"\"{aud.Path}\""); args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
} }
if (options.Subtitles.Count > 0){ if (options.Subtitles.Count > 0){
foreach (var subObj in options.Subtitles){ bool hasSignsSub = options.Subtitles.Any(sub => sub.Signs && options.Defaults.Sub.Code == sub.Language.Code);
var sortedSubtitles = options.Subtitles
.OrderBy(sub => CrunchyrollManager.Instance.CrunOptions.DlSubs.IndexOf(sub.Language.CrLocale) != -1 ? CrunchyrollManager.Instance.CrunOptions.DlSubs.IndexOf(sub.Language.CrLocale) : int.MaxValue)
.ThenBy(sub => sub.Signs ? 0 : 1)
.ThenBy(sub => sub.ClosedCaption ? 0 : 1)
.ToList();
foreach (var subObj in sortedSubtitles){
bool isForced = false; bool isForced = false;
if (subObj.Delay.HasValue){ if (subObj.Delay.HasValue){
double delay = subObj.Delay ?? 0; double delay = subObj.Delay ?? 0;
@ -202,7 +216,8 @@ public class Merger{
args.Add($"--track-name {trackName}"); args.Add($"--track-name {trackName}");
args.Add($"--language 0:\"{subObj.Language.Code}\""); args.Add($"--language 0:\"{subObj.Language.Code}\"");
if (options.Defaults.Sub.Code == subObj.Language.Code && CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == subObj.Signs && subObj.ClosedCaption == false){ if (options.Defaults.Sub.Code == subObj.Language.Code &&
(CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns == subObj.Signs || CrunchyrollManager.Instance.CrunOptions.DefaultSubSigns && !hasSignsSub) && subObj.ClosedCaption == false){
args.Add("--default-track 0"); args.Add("--default-track 0");
if (CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay){ if (CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay){
args.Add("--forced-track 0:yes"); args.Add("--forced-track 0:yes");
@ -211,16 +226,16 @@ public class Merger{
} else{ } else{
args.Add("--default-track 0:0"); args.Add("--default-track 0:0");
} }
if (subObj.ClosedCaption == true && CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag){ if (subObj.ClosedCaption == true && CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag){
args.Add("--hearing-impaired-flag 0:yes"); args.Add("--hearing-impaired-flag 0:yes");
} }
if (subObj.Signs == true && CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced && !isForced){ if (subObj.Signs && CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced && !isForced){
args.Add("--forced-track 0:yes"); args.Add("--forced-track 0:yes");
} }
args.Add($"\"{subObj.File}\""); args.Add($"\"{Helpers.AddUncPrefixIfNeeded(subObj.File)}\"");
} }
} else{ } else{
args.Add("--no-subtitles"); args.Add("--no-subtitles");
@ -230,14 +245,14 @@ public class Merger{
foreach (var font in options.Fonts){ foreach (var font in options.Fonts){
args.Add($"--attachment-name \"{font.Name}\""); args.Add($"--attachment-name \"{font.Name}\"");
args.Add($"--attachment-mime-type \"{font.Mime}\""); args.Add($"--attachment-mime-type \"{font.Mime}\"");
args.Add($"--attach-file \"{font.Path}\""); args.Add($"--attach-file \"{Helpers.AddUncPrefixIfNeeded(font.Path)}\"");
} }
} else{ } else{
args.Add("--no-attachments"); args.Add("--no-attachments");
} }
if (options.Chapters is{ Count: > 0 }){ if (options.Chapters is{ Count: > 0 }){
args.Add($"--chapters \"{options.Chapters[0].Path}\""); args.Add($"--chapters \"{Helpers.AddUncPrefixIfNeeded(options.Chapters[0].Path)}\"");
} }
if (!string.IsNullOrEmpty(options.VideoTitle)){ if (!string.IsNullOrEmpty(options.VideoTitle)){
@ -245,55 +260,105 @@ public class Merger{
} }
if (options.Description is{ Count: > 0 }){ if (options.Description is{ Count: > 0 }){
args.Add($"--global-tags \"{options.Description[0].Path}\""); args.Add($"--global-tags \"{Helpers.AddUncPrefixIfNeeded(options.Description[0].Path)}\"");
} }
return string.Join(" ", args); return string.Join(" ", args);
} }
public async Task<double> ProcessVideo(string baseVideoPath, string compareVideoPath){ public async Task<double> ProcessVideo(string baseVideoPath, string compareVideoPath){
string baseFramesDir; string baseFramesDir, baseFramesDirEnd;
string compareFramesDir; string compareFramesDir, compareFramesDirEnd;
string cleanupDir;
try{ try{
var tempDir = CfgManager.PathTEMP_DIR; var tempDir = CfgManager.PathTEMP_DIR;
baseFramesDir = Path.Combine(tempDir, "base_frames"); string uuid = Guid.NewGuid().ToString();
compareFramesDir = Path.Combine(tempDir, "compare_frames");
cleanupDir = Path.Combine(tempDir, uuid);
baseFramesDir = Path.Combine(tempDir, uuid, "base_frames_start");
baseFramesDirEnd = Path.Combine(tempDir, uuid, "base_frames_end");
compareFramesDir = Path.Combine(tempDir, uuid, "compare_frames_start");
compareFramesDirEnd = Path.Combine(tempDir, uuid, "compare_frames_end");
Directory.CreateDirectory(baseFramesDir); Directory.CreateDirectory(baseFramesDir);
Directory.CreateDirectory(baseFramesDirEnd);
Directory.CreateDirectory(compareFramesDir); Directory.CreateDirectory(compareFramesDir);
Directory.CreateDirectory(compareFramesDirEnd);
} catch (Exception e){ } catch (Exception e){
Console.Error.WriteLine(e); Console.Error.WriteLine(e);
return 0; return -100;
}
var extractFramesBase = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 60);
var extractFramesCompare = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 60);
if (!extractFramesBase.IsOk || !extractFramesCompare.IsOk){
Console.Error.WriteLine("Failed to extract Frames to Compare");
return 0;
} }
var baseFrames = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{ try{
FilePath = fp, var extractFramesBaseStart = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 120);
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate) var extractFramesCompareStart = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 120);
}).ToList();
var compareFrames = Directory.GetFiles(compareFramesDir).Select(fp => new FrameData{ TimeSpan? baseVideoDurationTimeSpan = await Helpers.GetMediaDurationAsync(CfgManager.PathFFMPEG, baseVideoPath);
FilePath = fp, TimeSpan? compareVideoDurationTimeSpan = await Helpers.GetMediaDurationAsync(CfgManager.PathFFMPEG, compareVideoPath);
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate)
}).ToList();
var offset = SyncingHelper.CalculateOffset(baseFrames, compareFrames); if (baseVideoDurationTimeSpan == null || compareVideoDurationTimeSpan == null){
Console.WriteLine($"Calculated offset: {offset} seconds"); Console.Error.WriteLine("Failed to retrieve video durations");
return -100;
}
var extractFramesBaseEnd = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDirEnd, baseVideoDurationTimeSpan.Value.TotalSeconds - 360, 360);
var extractFramesCompareEnd = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDirEnd, compareVideoDurationTimeSpan.Value.TotalSeconds - 360, 360);
CleanupDirectory(baseFramesDir); if (!extractFramesBaseStart.IsOk || !extractFramesCompareStart.IsOk || !extractFramesBaseEnd.IsOk || !extractFramesCompareEnd.IsOk){
CleanupDirectory(compareFramesDir); Console.Error.WriteLine("Failed to extract Frames to Compare");
return -100;
}
return offset; // Load frames from start of the videos
var baseFramesStart = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBaseStart.frameRate)
}).ToList();
var compareFramesStart = Directory.GetFiles(compareFramesDir).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesCompareStart.frameRate)
}).ToList();
// Load frames from end of the videos
var baseFramesEnd = Directory.GetFiles(baseFramesDirEnd).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBaseEnd.frameRate)
}).ToList();
var compareFramesEnd = Directory.GetFiles(compareFramesDirEnd).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesCompareEnd.frameRate)
}).ToList();
// Calculate offsets
var startOffset = SyncingHelper.CalculateOffset(baseFramesStart, compareFramesStart);
var endOffset = SyncingHelper.CalculateOffset(baseFramesEnd, compareFramesEnd,true);
var lengthDiff = Math.Abs(baseVideoDurationTimeSpan.Value.TotalMicroseconds - compareVideoDurationTimeSpan.Value.TotalMicroseconds) / 1000000;
endOffset += lengthDiff;
Console.WriteLine($"Start offset: {startOffset} seconds");
Console.WriteLine($"End offset: {endOffset} seconds");
CleanupDirectory(cleanupDir);
var difference = Math.Abs(startOffset - endOffset);
switch (difference){
case < 0.1:
return startOffset;
case > 1:
return -100;
default:
return endOffset;
}
} catch (Exception e){
Console.Error.WriteLine(e);
return -100;
}
} }
private static void CleanupDirectory(string dirPath){ private static void CleanupDirectory(string dirPath){
@ -365,8 +430,8 @@ public class MergerInput{
public class SubtitleInput{ public class SubtitleInput{
public LanguageItem Language{ get; set; } public LanguageItem Language{ get; set; }
public string File{ get; set; } public string File{ get; set; }
public bool? ClosedCaption{ get; set; } public bool ClosedCaption{ get; set; }
public bool? Signs{ get; set; } public bool Signs{ get; set; }
public int? Delay{ get; set; } public int? Delay{ get; set; }
public DownloadedMedia? RelatedVideoDownloadMedia; public DownloadedMedia? RelatedVideoDownloadMedia;

View file

@ -16,7 +16,7 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{ public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){ public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
var ffmpegPath = CfgManager.PathFFMPEG; var ffmpegPath = CfgManager.PathFFMPEG;
var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -vsync vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%03d.png\""; var arguments = $"-i \"{videoPath}\" -vf \"select='gt(scene,0.1)',showinfo\" -fps_mode vfr -frame_pts true -t {duration} -ss {offset} \"{outputDir}\\frame%03d.png\"";
var output = ""; var output = "";
@ -37,7 +37,7 @@ public class SyncingHelper{
process.ErrorDataReceived += (sender, e) => { process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){ if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine($"{e.Data}"); // Console.WriteLine($"{e.Data}");
output += e.Data; output += e.Data;
} }
}; };
@ -128,18 +128,35 @@ public class SyncingHelper{
float[] pixels1 = ExtractPixels(image1, targetWidth, targetHeight); float[] pixels1 = ExtractPixels(image1, targetWidth, targetHeight);
float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight); float[] pixels2 = ExtractPixels(image2, targetWidth, targetHeight);
// Check if any frame is completely black, if so, skip SSIM calculation
if (IsBlackFrame(pixels1) || IsBlackFrame(pixels2)){
// Return a negative value or zero to indicate no SSIM comparison for black frames.
return -1.0;
}
// Compute SSIM // Compute SSIM
return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight); return CalculateSSIM(pixels1, pixels2, targetWidth, targetHeight);
} }
} }
private static bool IsBlackFrame(float[] pixels, float threshold = 1.0f){
// Check if all pixel values are below the threshold, indicating a black frame.
return pixels.All(p => p <= threshold);
}
public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){ public static bool AreFramesSimilar(string imagePath1, string imagePath2, double ssimThreshold){
double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256); double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256);
// Console.WriteLine($"SSIM: {ssim}"); // Console.WriteLine($"SSIM: {ssim}");
return ssim > ssimThreshold; return ssim > ssimThreshold;
} }
public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames, double ssimThreshold = 0.9){ public static double CalculateOffset(List<FrameData> baseFrames, List<FrameData> compareFrames,bool reverseCompare = false, double ssimThreshold = 0.9){
if (reverseCompare){
baseFrames.Reverse();
compareFrames.Reverse();
}
foreach (var baseFrame in baseFrames){ foreach (var baseFrame in baseFrames){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold)); var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
if (matchingFrame != null){ if (matchingFrame != null){

View file

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic;
using System.Xml; using System.Xml;
using CRD.Utils.Parser.Utils; using CRD.Utils.Parser.Utils;
using Newtonsoft.Json;
namespace CRD.Utils.Parser; namespace CRD.Utils.Parser;

View file

@ -1,9 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic;
using System.Linq; using System.Linq;
using System.Xml.Linq; using System.Xml.Linq;
using CRD.Downloader;
using CRD.Utils.HLS; using CRD.Utils.HLS;
using CRD.Utils.Parser; using CRD.Utils.Parser;
using CRD.Utils.Parser.Utils; using CRD.Utils.Parser.Utils;

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq; using System.Linq;
using System.Xml; using System.Xml;
using Avalonia.Logging;
using CRD.Utils.Parser.Utils; using CRD.Utils.Parser.Utils;
namespace CRD.Utils.Parser; namespace CRD.Utils.Parser;

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq;
using System.Xml; using System.Xml;
using CRD.Utils.Parser.Utils; using CRD.Utils.Parser.Utils;

View file

@ -1,6 +1,4 @@
using System.Collections.Generic; namespace CRD.Utils.Parser.Utils;
namespace CRD.Utils.Parser.Utils;
public class ManifestInfo{ public class ManifestInfo{
public dynamic locations{ get; set; } public dynamic locations{ get; set; }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Dynamic; using System.Dynamic;
using System.Linq;
namespace CRD.Utils.Parser.Utils; namespace CRD.Utils.Parser.Utils;

View file

@ -1,7 +1,4 @@
using System.Text.RegularExpressions; namespace CRD.Utils.Parser.Utils;
using System;
namespace CRD.Utils.Parser.Utils;
public class UrlResolver{ public class UrlResolver{

View file

@ -2,17 +2,14 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics; using System.Diagnostics;
using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Sonarr.Models; using CRD.Utils.Sonarr.Models;
using CRD.Views; using CRD.Views;
using Newtonsoft.Json;
namespace CRD.Utils.Sonarr; namespace CRD.Utils.Sonarr;

View file

@ -0,0 +1,41 @@
using System.Collections.Generic;
namespace CRD.Utils.Structs.History;
public class AniListResponse{
public Data? Data{ get; set; }
}
public class Data{
public Page? Page{ get; set; }
}
public class Page{
public PageInfo? PageInfo{ get; set; }
public List<AnilistSeries>? Media{ get; set; }
}
public class PageInfo{
public bool HasNextPage{ get; set; }
public int Total{ get; set; }
}
public class AniListResponseCalendar{
public Data2? Data{ get; set; }
}
public class Data2{
public Page2? Page{ get; set; }
}
public class Page2{
public PageInfo? PageInfo{ get; set; }
public List<AiringSchedule>? AiringSchedules{ get; set; }
}
public class AiringSchedule{
public int Id{ get; set; }
public int Episode{ get; set; }
public int AiringAt{ get; set; }
public AnilistSeries? Media{ get; set; }
}

View file

@ -0,0 +1,126 @@
using System.Collections.Generic;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public partial class AnilistSeries : ObservableObject{
public int Id{ get; set; }
public int? IdMal{ get; set; }
public Title Title{ get; set; }
public Date StartDate{ get; set; }
public Date EndDate{ get; set; }
public string Status{ get; set; }
public string Season{ get; set; }
public string Format{ get; set; }
public List<string> Genres{ get; set; }
public List<string> Synonyms{ get; set; }
public int? Duration{ get; set; }
public int Popularity{ get; set; }
public int? Episodes{ get; set; }
public string Source{ get; set; }
public string CountryOfOrigin{ get; set; }
public string Hashtag{ get; set; }
public int? AverageScore{ get; set; }
public string SiteUrl{ get; set; }
public string Description{ get; set; }
public string BannerImage{ get; set; }
public bool IsAdult{ get; set; }
public CoverImage CoverImage{ get; set; }
public Trailer Trailer{ get; set; }
public List<ExternalLink>? ExternalLinks{ get; set; }
public List<Ranking> Rankings{ get; set; }
public Studios Studios{ get; set; }
public Relations Relations{ get; set; }
public AiringSchedule AiringSchedule{ get; set; }
[JsonIgnore]
public Bitmap? ThumbnailImage{ get; set; }
[JsonIgnore]
public string StartDateForm => $"{StartDate.Day}.{StartDate.Month}.{StartDate.Year}";
[JsonIgnore]
public string? CrunchyrollID;
[JsonIgnore]
[ObservableProperty]
public bool _hasCrID;
[JsonIgnore]
[ObservableProperty]
public bool _isInHistory;
}
public class Title{
public string Romaji{ get; set; }
public string Native{ get; set; }
public string English{ get; set; }
}
public class Date{
public int? Year{ get; set; }
public int? Month{ get; set; }
public int? Day{ get; set; }
}
public class CoverImage{
public string ExtraLarge{ get; set; }
public string Color{ get; set; }
}
public class Trailer{
public string Id{ get; set; }
public string Site{ get; set; }
public string Thumbnail{ get; set; }
}
public class ExternalLink{
public string Site{ get; set; }
public string Icon{ get; set; }
public string Color{ get; set; }
public string Url{ get; set; }
}
public class Ranking{
public int Rank{ get; set; }
public string Type{ get; set; }
public string Season{ get; set; }
public bool AllTime{ get; set; }
}
public class Studios{
public List<StudioNode> Nodes{ get; set; }
}
public class StudioNode{
public int Id{ get; set; }
public string Name{ get; set; }
public string SiteUrl{ get; set; }
}
public class Relations{
public List<RelationEdge> Edges{ get; set; }
}
public class RelationEdge{
public string RelationType{ get; set; }
public RelationNode Node{ get; set; }
}
public class RelationNode{
public int Id{ get; set; }
public Title Title{ get; set; }
public string SiteUrl{ get; set; }
}
public class AiringSchedule{
public List<AiringNode> Nodes{ get; set; }
}
public class AiringNode{
public int Episode{ get; set; }
public long AiringAt{ get; set; }
}

View file

@ -1,13 +1,9 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Net.Http;
using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
@ -15,19 +11,19 @@ using CRD.Downloader.Crunchyroll;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
public class CalendarWeek{ public class CalendarWeek{
public DateTime? FirstDayOfWeek{ get; set; } public DateTime FirstDayOfWeek{ get; set; }
public string? FirstDayOfWeekString{ get; set; } public string? FirstDayOfWeekString{ get; set; }
public List<CalendarDay>? CalendarDays{ get; set; } public List<CalendarDay>? CalendarDays{ get; set; }
} }
public class CalendarDay{ public class CalendarDay{
public DateTime? DateTime{ get; set; } public DateTime DateTime{ get; set; }
public string? DayName{ get; set; } public string? DayName{ get; set; }
public List<CalendarEpisode>? CalendarEpisodes{ get; set; } public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
} }
public partial class CalendarEpisode : INotifyPropertyChanged{ public partial class CalendarEpisode : INotifyPropertyChanged{
public DateTime? DateTime{ get; set; } public DateTime DateTime{ get; set; }
public bool? HasPassed{ get; set; } public bool? HasPassed{ get; set; }
public string? EpisodeName{ get; set; } public string? EpisodeName{ get; set; }
public string? SeriesUrl{ get; set; } public string? SeriesUrl{ get; set; }
@ -42,32 +38,38 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
public string? SeasonName{ get; set; } public string? SeasonName{ get; set; }
public string? CrSeriesID{ get; set; }
public bool AnilistEpisode{ get; set; }
public List<CalendarEpisode> CalendarEpisodes{ get; set; } =[];
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
[RelayCommand] [RelayCommand]
public void AddEpisodeToQue(string episodeUrl){ public void AddEpisodeToQue(){
var match = Regex.Match(episodeUrl, "/([^/]+)/watch/([^/]+)"); if (CalendarEpisodes.Count > 0){
foreach (var calendarEpisode in CalendarEpisodes){
calendarEpisode.AddEpisodeToQue();
}
}
if (match.Success){ if (EpisodeUrl != null){
var locale = match.Groups[1].Value; // Capture the locale part var match = Regex.Match(EpisodeUrl, "/([^/]+)/watch/([^/]+)");
var id = match.Groups[2].Value; // Capture the ID part
QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true); if (match.Success){
var locale = match.Groups[1].Value; // Capture the locale part
var id = match.Groups[2].Value; // Capture the ID part
QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true);
}
} }
} }
public async Task LoadImage(){ public async Task LoadImage(int width = 0, int height = 0){
try{ try{
if (string.IsNullOrEmpty(ThumbnailUrl)){ if (!string.IsNullOrEmpty(ThumbnailUrl)){
ImageBitmap = await Helpers.LoadImage(ThumbnailUrl, width, height);
} else{ PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
using (var client = new HttpClient()){
var response = await client.GetAsync(ThumbnailUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ImageBitmap = new Bitmap(stream);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}
} }
} catch (Exception ex){ } catch (Exception ex){
// Handle exceptions // Handle exceptions

View file

@ -6,7 +6,7 @@ namespace CRD.Utils.Structs;
public struct CrunchyChapters{ public struct CrunchyChapters{
public List<CrunchyChapter> Chapters { get; set; } public List<CrunchyChapter> Chapters { get; set; }
public DateTime? lastUpdate { get; set; } public DateTime lastUpdate { get; set; }
public string? mediaId { get; set; } public string? mediaId { get; set; }
} }

View file

@ -6,15 +6,98 @@ using YamlDotNet.Serialization;
namespace CRD.Utils.Structs; namespace CRD.Utils.Structs;
public class CrDownloadOptions{ public class CrDownloadOptions{
#region General Settings
[YamlMember(Alias = "auto_download", ApplyNamingConventions = false)] [YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
public bool AutoDownload{ get; set; } public bool AutoDownload{ get; set; }
[YamlMember(Alias = "remove_finished_downloads", ApplyNamingConventions = false)] [YamlMember(Alias = "remove_finished_downloads", ApplyNamingConventions = false)]
public bool RemoveFinishedDownload{ get; set; } public bool RemoveFinishedDownload{ get; set; }
[YamlIgnore]
public int Timeout{ get; set; }
[YamlIgnore]
public int FsRetryTime{ get; set; }
[YamlIgnore]
public string Force{ get; set; }
[YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
public int SimultaneousDownloads{ get; set; }
[YamlMember(Alias = "theme", ApplyNamingConventions = false)]
public string Theme{ get; set; }
[YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
public string? AccentColor{ get; set; }
[YamlMember(Alias = "background_image_path", ApplyNamingConventions = false)]
public string? BackgroundImagePath{ get; set; }
[YamlMember(Alias = "background_image_opacity", ApplyNamingConventions = false)]
public double BackgroundImageOpacity{ get; set; }
[YamlMember(Alias = "background_image_blur_radius", ApplyNamingConventions = false)]
public double BackgroundImageBlurRadius{ get; set; }
[YamlIgnore]
public List<string> Override{ get; set; }
[YamlIgnore]
public string CcTag{ get; set; }
[YamlIgnore]
public bool Nocleanup{ get; set; }
[YamlMember(Alias = "history", ApplyNamingConventions = false)]
public bool History{ get; set; }
[YamlMember(Alias = "history_lang", ApplyNamingConventions = false)]
public string? HistoryLang{ get; set; }
[YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
public bool HistoryAddSpecials{ get; set; }
[YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
public bool HistoryCountSonarr{ get; set; }
[YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
public SonarrProperties? SonarrProperties{ get; set; }
[YamlMember(Alias = "log_mode", ApplyNamingConventions = false)]
public bool LogMode{ get; set; }
[YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)]
public string? DownloadDirPath{ get; set; }
[YamlMember(Alias = "download_temp_dir_path", ApplyNamingConventions = false)]
public string? DownloadTempDirPath{ get; set; }
[YamlMember(Alias = "download_to_temp_folder", ApplyNamingConventions = false)]
public bool DownloadToTempFolder{ get; set; }
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
[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; }
#endregion
#region Crunchyroll Settings
[YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "hard_sub_lang", ApplyNamingConventions = false)]
public string Hslang{ get; set; } public string Hslang{ get; set; }
@ -45,14 +128,6 @@ public class CrDownloadOptions{
[YamlIgnore] [YamlIgnore]
public int Partsize{ get; set; } public int Partsize{ get; set; }
[YamlIgnore]
public int Timeout{ get; set; }
[YamlIgnore]
public int Waittime{ get; set; }
[YamlIgnore]
public int FsRetryTime{ get; set; }
[YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)] [YamlMember(Alias = "soft_subs", ApplyNamingConventions = false)]
public List<string> DlSubs{ get; set; } public List<string> DlSubs{ get; set; }
@ -62,43 +137,37 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)]
public bool SkipSubsMux{ get; set; } public bool SkipSubsMux{ get; set; }
[YamlMember(Alias = "subs_add_scaled_border", ApplyNamingConventions = false)] [YamlMember(Alias = "subs_add_scaled_border", ApplyNamingConventions = false)]
public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; } public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; }
[YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)] [YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)]
public bool IncludeSignsSubs{ get; set; } public bool IncludeSignsSubs{ get; set; }
[YamlMember(Alias = "mux_signs_subs_flag", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_signs_subs_flag", ApplyNamingConventions = false)]
public bool SignsSubsAsForced{ get; set; } public bool SignsSubsAsForced{ get; set; }
[YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)] [YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)]
public bool IncludeCcSubs{ get; set; } public bool IncludeCcSubs{ get; set; }
[YamlMember(Alias = "cc_subs_font", ApplyNamingConventions = false)] [YamlMember(Alias = "cc_subs_font", ApplyNamingConventions = false)]
public string? CcSubsFont{ get; set; } public string? CcSubsFont{ get; set; }
[YamlMember(Alias = "mux_cc_subs_flag", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_cc_subs_flag", ApplyNamingConventions = false)]
public bool CcSubsMuxingFlag{ get; set; } public bool CcSubsMuxingFlag{ get; set; }
[YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)]
public bool Mp4{ get; set; } public bool Mp4{ get; set; }
[YamlIgnore]
public List<string> Override{ get; set; }
[YamlMember(Alias = "mux_video_title", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_video_title", ApplyNamingConventions = false)]
public string? VideoTitle{ get; set; } public string? VideoTitle{ get; set; }
[YamlMember(Alias = "mux_video_description", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_video_description", ApplyNamingConventions = false)]
public bool IncludeVideoDescription{ get; set; } public bool IncludeVideoDescription{ get; set; }
[YamlMember(Alias = "mux_description_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_description_lang", ApplyNamingConventions = false)]
public string? DescriptionLang{ get; set; } public string? DescriptionLang{ get; set; }
[YamlIgnore]
public string Force{ get; set; }
[YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)]
public List<string> FfmpegOptions{ get; set; } public List<string> FfmpegOptions{ get; set; }
@ -107,22 +176,19 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)]
public string DefaultSub{ get; set; } public string DefaultSub{ get; set; }
[YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)]
public bool DefaultSubSigns{ get; set; } public bool DefaultSubSigns{ get; set; }
[YamlMember(Alias = "mux_default_sub_forced_display", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_default_sub_forced_display", ApplyNamingConventions = false)]
public bool DefaultSubForcedDisplay{ get; set; } public bool DefaultSubForcedDisplay{ get; set; }
[YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
public string DefaultAudio{ get; set; } public string DefaultAudio{ get; set; }
[YamlIgnore]
public string CcTag{ get; set; }
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)] [YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; } public bool DlVideoOnce{ get; set; }
[YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)] [YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)]
public bool KeepDubsSeperate{ get; set; } public bool KeepDubsSeperate{ get; set; }
@ -131,88 +197,37 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)] [YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)]
public bool SyncTiming{ get; set; } public bool SyncTiming{ get; set; }
[YamlMember(Alias = "encode_enabled", ApplyNamingConventions = false)] [YamlMember(Alias = "encode_enabled", ApplyNamingConventions = false)]
public bool IsEncodeEnabled{ get; set; } public bool IsEncodeEnabled{ get; set; }
[YamlMember(Alias = "encode_preset", ApplyNamingConventions = false)] [YamlMember(Alias = "encode_preset", ApplyNamingConventions = false)]
public string? EncodingPresetName{ get; set; } public string? EncodingPresetName{ get; set; }
[YamlIgnore]
public bool Nocleanup{ get; set; }
[YamlMember(Alias = "chapters", ApplyNamingConventions = false)] [YamlMember(Alias = "chapters", ApplyNamingConventions = false)]
public bool Chapters{ get; set; } public bool Chapters{ get; set; }
[YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)] [YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)]
public List<string> DubLang{ get; set; } public List<string> DubLang{ get; set; }
[YamlMember(Alias = "simultaneous_downloads", ApplyNamingConventions = false)]
public int SimultaneousDownloads{ get; set; }
[YamlMember(Alias = "theme", ApplyNamingConventions = false)]
public string Theme{ get; set; }
[YamlMember(Alias = "accent_color", ApplyNamingConventions = false)]
public string? AccentColor{ get; set; }
[YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_language", ApplyNamingConventions = false)]
public string? SelectedCalendarLanguage{ get; set; } public string? SelectedCalendarLanguage{ get; set; }
[YamlMember(Alias = "calendar_dub_filter", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_dub_filter", ApplyNamingConventions = false)]
public string? CalendarDubFilter{ get; set; } public string? CalendarDubFilter{ get; set; }
[YamlMember(Alias = "calendar_custom", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_custom", ApplyNamingConventions = false)]
public bool CustomCalendar{ get; set; } public bool CustomCalendar{ get; set; }
[YamlMember(Alias = "calendar_hide_dubs", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_hide_dubs", ApplyNamingConventions = false)]
public bool CalendarHideDubs{ get; set; } public bool CalendarHideDubs{ get; set; }
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)] [YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
public bool CalendarFilterByAirDate{ get; set; } public bool CalendarFilterByAirDate{ get; set; }
[YamlMember(Alias = "history", ApplyNamingConventions = false)]
public bool History{ get; set; }
[YamlMember(Alias = "history_lang", ApplyNamingConventions = false)]
public string? HistoryLang{ get; set; }
[YamlMember(Alias = "history_add_specials", ApplyNamingConventions = false)]
public bool HistoryAddSpecials{ get; set; }
[YamlMember(Alias = "history_count_sonarr", ApplyNamingConventions = false)]
public bool HistoryCountSonarr{ get; set; }
[YamlMember(Alias = "sonarr_properties", ApplyNamingConventions = false)]
public SonarrProperties? SonarrProperties{ get; set; }
[YamlMember(Alias = "log_mode", ApplyNamingConventions = false)]
public bool LogMode{ get; set; }
[YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)] [YamlMember(Alias = "stream_endpoint", ApplyNamingConventions = false)]
public string? StreamEndpoint{ get; set; } public string? StreamEndpoint{ get; set; }
[YamlMember(Alias = "download_dir_path", ApplyNamingConventions = false)] #endregion
public string? DownloadDirPath{ get; set; }
[YamlMember(Alias = "download_temp_dir_path", ApplyNamingConventions = false)]
public string? DownloadTempDirPath{ get; set; }
[YamlMember(Alias = "download_to_temp_folder", ApplyNamingConventions = false)]
public bool DownloadToTempFolder{ get; set; }
[YamlMember(Alias = "history_page_properties", ApplyNamingConventions = false)]
public HistoryPageProperties? HistoryPageProperties{ get; set; }
[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

@ -27,16 +27,16 @@ public class CrunchyMovie{
public bool IsMature{ get; set; } public bool IsMature{ get; set; }
[JsonProperty("free_available_date")] [JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; } public DateTime FreeAvailableDate{ get; set; }
[JsonProperty("premium_available_date")] [JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; } public DateTime PremiumAvailableDate{ get; set; }
[JsonProperty("availability_starts")] [JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; } public DateTime AvailabilityStarts{ get; set; }
[JsonProperty("availability_ends")] [JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; } public DateTime AvailabilityEnds{ get; set; }
[JsonProperty("maturity_ratings")] [JsonProperty("maturity_ratings")]
public List<string> MaturityRatings{ get; set; } public List<string> MaturityRatings{ get; set; }
@ -55,7 +55,7 @@ public class CrunchyMovie{
public string ListingId{ get; set; } public string ListingId{ get; set; }
[JsonProperty("available_date")] [JsonProperty("available_date")]
public DateTime? AvailableDate{ get; set; } public DateTime AvailableDate{ get; set; }
[JsonProperty("is_subbed")] [JsonProperty("is_subbed")]
public bool IsSubbed{ get; set; } public bool IsSubbed{ get; set; }
@ -94,5 +94,5 @@ public class CrunchyMovie{
public Dictionary<string, object> ExtendedMaturityRating{ get; set; } public Dictionary<string, object> ExtendedMaturityRating{ get; set; }
[JsonProperty("premium_date")] [JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; } public DateTime PremiumDate{ get; set; }
} }

View file

@ -8,6 +8,8 @@ public class CrProfile{
public string? Avatar{ get; set; } public string? Avatar{ get; set; }
public string? Email{ get; set; } public string? Email{ get; set; }
public string? Username{ get; set; } public string? Username{ get; set; }
[JsonProperty("profile_name")]
public string? ProfileName{ get; set; }
[JsonProperty("preferred_content_audio_language")] [JsonProperty("preferred_content_audio_language")]
public string? PreferredContentAudioLanguage{ get; set; } public string? PreferredContentAudioLanguage{ get; set; }

View file

@ -11,5 +11,6 @@ public class CrToken{
public string? country { get; set; } public string? country { get; set; }
public string? account_id { get; set; } public string? account_id { get; set; }
public string? profile_id { get; set; } public string? profile_id { get; set; }
public DateTime? expires { get; set; } public string? device_id { get; set; }
public DateTime expires { get; set; }
} }

View file

@ -121,25 +121,25 @@ public class CrBrowseEpisodeMetaData{
public double SequenceNumber{ get; set; } public double SequenceNumber{ get; set; }
[JsonProperty("upload_date")] [JsonProperty("upload_date")]
public DateTime? UploadDate{ get; set; } public DateTime UploadDate{ get; set; }
[JsonProperty("subtitle_locales")] [JsonProperty("subtitle_locales")]
public List<Locale>? SubtitleLocales{ get; set; } public List<Locale>? SubtitleLocales{ get; set; }
[JsonProperty("premium_available_date")] [JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; } public DateTime PremiumAvailableDate{ get; set; }
[JsonProperty("availability_ends")] [JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; } public DateTime AvailabilityEnds{ get; set; }
[JsonProperty("availability_starts")] [JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; } public DateTime AvailabilityStarts{ get; set; }
[JsonProperty("free_available_date")] [JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; } public DateTime FreeAvailableDate{ get; set; }
[JsonProperty("identifier")] [JsonProperty("identifier")]
public string? Identifier{ get; set; } public string? Identifier{ get; set; }
@ -157,10 +157,10 @@ public class CrBrowseEpisodeMetaData{
public string? EligibleRegion{ get; set; } public string? EligibleRegion{ get; set; }
[JsonProperty("available_date")] [JsonProperty("available_date")]
public DateTime? AvailableDate{ get; set; } public DateTime AvailableDate{ get; set; }
[JsonProperty("premium_date")] [JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; } public DateTime PremiumDate{ get; set; }
[JsonProperty("available_offline")] [JsonProperty("available_offline")]
public bool AvailableOffline{ get; set; } public bool AvailableOffline{ get; set; }

View file

@ -48,7 +48,7 @@ public struct CrunchyEpisode{
public string EligibleRegion{ get; set; } public string EligibleRegion{ get; set; }
[JsonProperty("availability_starts")] [JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; } public DateTime AvailabilityStarts{ get; set; }
public Images? Images{ get; set; } public Images? Images{ get; set; }
@ -70,7 +70,7 @@ public struct CrunchyEpisode{
public string ProductionEpisodeId{ get; set; } public string ProductionEpisodeId{ get; set; }
[JsonProperty("premium_available_date")] [JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; } public DateTime PremiumAvailableDate{ get; set; }
[JsonProperty("season_title")] [JsonProperty("season_title")]
public string SeasonTitle{ get; set; } public string SeasonTitle{ get; set; }
@ -87,10 +87,10 @@ public struct CrunchyEpisode{
public string? MediaType{ get; set; } public string? MediaType{ get; set; }
[JsonProperty("availability_ends")] [JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; } public DateTime AvailabilityEnds{ get; set; }
[JsonProperty("free_available_date")] [JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; } public DateTime FreeAvailableDate{ get; set; }
public string Playback{ get; set; } public string Playback{ get; set; }
@ -106,12 +106,12 @@ public struct CrunchyEpisode{
public string ListingId{ get; set; } public string ListingId{ get; set; }
[JsonProperty("episode_air_date")] [JsonProperty("episode_air_date")]
public DateTime? EpisodeAirDate{ get; set; } public DateTime EpisodeAirDate{ get; set; }
public string Slug{ get; set; } public string Slug{ get; set; }
[JsonProperty("available_date")] [JsonProperty("available_date")]
public DateTime? AvailableDate{ get; set; } public DateTime AvailableDate{ get; set; }
[JsonProperty("subtitle_locales")] [JsonProperty("subtitle_locales")]
public List<string> SubtitleLocales{ get; set; } public List<string> SubtitleLocales{ get; set; }
@ -128,10 +128,10 @@ public struct CrunchyEpisode{
public bool IsSubbed{ get; set; } public bool IsSubbed{ get; set; }
[JsonProperty("premium_date")] [JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; } public DateTime PremiumDate{ get; set; }
[JsonProperty("upload_date")] [JsonProperty("upload_date")]
public DateTime? UploadDate{ get; set; } public DateTime UploadDate{ get; set; }
[JsonProperty("season_slug_title")] [JsonProperty("season_slug_title")]
public string SeasonSlugTitle{ get; set; } public string SeasonSlugTitle{ get; set; }
@ -247,13 +247,18 @@ public class CrunchyEpMeta{
public List<string>? SelectedDubs{ get; set; } public List<string>? SelectedDubs{ get; set; }
public string Hslang{ get; set; } = "none";
public List<string>? AvailableSubs{ get; set; } public List<string>? AvailableSubs{ get; set; }
public string? DownloadPath{ get; set; } public string? DownloadPath{ get; set; }
public string? VideoQuality{ get; set; }
public List<string> DownloadSubs{ get; set; } =[]; public List<string> DownloadSubs{ get; set; } =[];
public bool Music{ get; set; } public bool Music{ get; set; }
public string Resolution{ get; set; }
public List<string> downloadedFiles{ get; set; } =[];
} }
public class DownloadProgress{ public class DownloadProgress{
@ -274,7 +279,6 @@ public struct CrunchyEpMetaData{
public List<EpisodeVersion>? Versions{ get; set; } public List<EpisodeVersion>? Versions{ get; set; }
public bool IsSubbed{ get; set; } public bool IsSubbed{ get; set; }
public bool IsDubbed{ get; set; } public bool IsDubbed{ get; set; }
} }
public struct CrunchyRollEpisodeData{ public struct CrunchyRollEpisodeData{

View file

@ -43,7 +43,7 @@ public class CrunchyMusicVideo{
public bool MatureBlocked{ get; set; } public bool MatureBlocked{ get; set; }
[JsonProperty("originalRelease")] [JsonProperty("originalRelease")]
public DateTime? OriginalRelease{ get; set; } public DateTime OriginalRelease{ get; set; }
[JsonProperty("sequenceNumber")] [JsonProperty("sequenceNumber")]
public int SequenceNumber{ get; set; } public int SequenceNumber{ get; set; }
@ -76,7 +76,7 @@ public class CrunchyMusicVideo{
public bool IsPublic{ get; set; } public bool IsPublic{ get; set; }
[JsonProperty("publishDate")] [JsonProperty("publishDate")]
public DateTime? PublishDate{ get; set; } public DateTime PublishDate{ get; set; }
[JsonProperty("displayArtistName")] [JsonProperty("displayArtistName")]
public string? DisplayArtistName{ get; set; } public string? DisplayArtistName{ get; set; }
@ -91,12 +91,12 @@ public class CrunchyMusicVideo{
public string? Id{ get; set; } public string? Id{ get; set; }
[JsonProperty("createdAt")] [JsonProperty("createdAt")]
public DateTime? CreatedAt{ get; set; } public DateTime CreatedAt{ get; set; }
public MusicImages? Images{ get; set; } public MusicImages? Images{ get; set; }
[JsonProperty("updatedAt")] [JsonProperty("updatedAt")]
public DateTime? UpdatedAt{ get; set; } public DateTime UpdatedAt{ get; set; }
} }
@ -125,8 +125,8 @@ public struct MusicVideoArtist{
public struct MusicVideoAvailability{ public struct MusicVideoAvailability{
[JsonProperty("endDate")] [JsonProperty("endDate")]
public DateTime? EndDate{ get; set; } public DateTime EndDate{ get; set; }
[JsonProperty("startDate")] [JsonProperty("startDate")]
public DateTime? StartDate{ get; set; } public DateTime StartDate{ get; set; }
} }

View file

@ -53,6 +53,9 @@ public class CrBrowseSeries : INotifyPropertyChanged{
[JsonIgnore] [JsonIgnore]
public Bitmap? ImageBitmap{ get; set; } public Bitmap? ImageBitmap{ get; set; }
[JsonIgnore]
public bool IsInHistory{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){ public async void LoadImage(string url){

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.Crunchyroll; namespace CRD.Utils.Structs.Crunchyroll;

View file

@ -1,4 +1,5 @@
using System.Collections.Generic; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.Threading.Tasks; using System.Threading.Tasks;
using CRD.Downloader; using CRD.Downloader;
@ -23,6 +24,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_cr_season_number")] [JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; } public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_cr_premium_air_date")]
public DateTime? EpisodeCrPremiumAirDate{ get; set; }
[JsonProperty("episode_was_downloaded")] [JsonProperty("episode_was_downloaded")]
public bool WasDownloaded{ get; set; } public bool WasDownloaded{ get; set; }

View file

@ -1,10 +1,8 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using Avalonia.Controls;
using CRD.Downloader;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace CRD.Utils.Structs.History; namespace CRD.Utils.Structs.History;
@ -31,6 +29,9 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("series_download_path")] [JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; } public string? SeasonDownloadPath{ get; set; }
[JsonProperty("history_season_video_quality_override")]
public string HistorySeasonVideoQualityOverride{ get; set; } = "";
[JsonProperty("history_season_soft_subs_override")] [JsonProperty("history_season_soft_subs_override")]
public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[]; public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
@ -45,7 +46,22 @@ public class HistorySeason : INotifyPropertyChanged{
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
#region Language Override #region Settings Override
[JsonIgnore]
public StringItem? _selectedVideoQualityItem;
[JsonIgnore]
public StringItem? SelectedVideoQualityItem{
get => _selectedVideoQualityItem;
set{
_selectedVideoQualityItem = value;
HistorySeasonVideoQualityOverride = value?.stringValue ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedVideoQualityItem)));
CfgManager.UpdateHistoryFile();
}
}
[JsonIgnore] [JsonIgnore]
public string SelectedSubs{ get; set; } = ""; public string SelectedSubs{ get; set; } = "";
@ -68,7 +84,18 @@ public class HistorySeason : INotifyPropertyChanged{
new StringItem(){ stringValue = "all" }, new StringItem(){ stringValue = "all" },
new StringItem(){ stringValue = "none" }, new StringItem(){ stringValue = "none" },
}; };
[JsonIgnore]
public ObservableCollection<StringItem> VideoQualityList{ get; } = new(){
new StringItem(){ stringValue = "best" },
new StringItem(){ stringValue = "1080p" },
new StringItem(){ stringValue = "720p" },
new StringItem(){ stringValue = "480p" },
new StringItem(){ stringValue = "360p" },
new StringItem(){ stringValue = "240p" },
new StringItem(){ stringValue = "worst" },
};
private void UpdateSubAndDubString(){ private void UpdateSubAndDubString(){
HistorySeasonSoftSubsOverride.Clear(); HistorySeasonSoftSubsOverride.Clear();
HistorySeasonDubLangOverride.Clear(); HistorySeasonDubLangOverride.Clear();
@ -88,6 +115,7 @@ public class HistorySeason : INotifyPropertyChanged{
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? ""; SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? ""; SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
@ -99,35 +127,35 @@ public class HistorySeason : INotifyPropertyChanged{
} }
public void Init(){ public void Init(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){ if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
foreach (var languageItem in Languages.languages){ foreach (var languageItem in Languages.languages){
SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); SubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale }); DubLangList.Add(new StringItem{ stringValue = languageItem.CrLocale });
} }
} }
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(a => HistorySeasonVideoQualityOverride.Equals(a.stringValue)) ?? new StringItem(){ stringValue = "" };
var softSubLang = SubLangList.Where(a => HistorySeasonSoftSubsOverride.Contains(a.stringValue)).ToList(); var softSubLang = SubLangList.Where(a => HistorySeasonSoftSubsOverride.Contains(a.stringValue)).ToList();
var dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList(); var dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList();
SelectedSubLang.Clear(); SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){ foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem); SelectedSubLang.Add(listBoxItem);
} }
SelectedDubLang.Clear(); SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){ foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem); SelectedDubLang.Add(listBoxItem);
} }
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? ""; SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? ""; SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
SelectedSubLang.CollectionChanged += Changes; SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes; SelectedDubLang.CollectionChanged += Changes;
} }
#endregion #endregion
public void UpdateDownloaded(string? EpisodeId){ public void UpdateDownloaded(string? EpisodeId){
@ -151,5 +179,4 @@ public class HistorySeason : INotifyPropertyChanged{
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count; DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
} }
} }

View file

@ -1,14 +1,11 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel; using System.ComponentModel;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.CustomList; using CRD.Utils.CustomList;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -52,12 +49,15 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("history_series_add_date")] [JsonProperty("history_series_add_date")]
public DateTime? HistorySeriesAddDate{ get; set; } public DateTime? HistorySeriesAddDate{ get; set; }
[JsonProperty("history_series_video_quality_override")]
public string HistorySeriesVideoQualityOverride{ get; set; } = "";
[JsonProperty("history_series_available_soft_subs")] [JsonProperty("history_series_available_soft_subs")]
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[]; public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_series_available_dub_lang")] [JsonProperty("history_series_available_dub_lang")]
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[]; public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
[JsonProperty("history_series_soft_subs_override")] [JsonProperty("history_series_soft_subs_override")]
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[]; public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
@ -92,7 +92,22 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore] [JsonIgnore]
private bool _editModeEnabled; private bool _editModeEnabled;
#region Language Override #region Settings Override
[JsonIgnore]
public StringItem? _selectedVideoQualityItem;
[JsonIgnore]
public StringItem? SelectedVideoQualityItem{
get => _selectedVideoQualityItem;
set{
_selectedVideoQualityItem = value;
HistorySeriesVideoQualityOverride = value?.stringValue ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedVideoQualityItem)));
CfgManager.UpdateHistoryFile();
}
}
[JsonIgnore] [JsonIgnore]
public string SelectedSubs{ get; set; } = ""; public string SelectedSubs{ get; set; } = "";
@ -107,6 +122,27 @@ public class HistorySeries : INotifyPropertyChanged{
public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new(); public ObservableCollection<StringItem> SelectedDubLang{ get; set; } = new();
[JsonIgnore]
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
};
[JsonIgnore]
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
new StringItem(){ stringValue = "all" },
new StringItem(){ stringValue = "none" },
};
[JsonIgnore]
public ObservableCollection<StringItem> VideoQualityList{ get; } = new(){
new StringItem(){ stringValue = "best" },
new StringItem(){ stringValue = "1080p" },
new StringItem(){ stringValue = "720p" },
new StringItem(){ stringValue = "480p" },
new StringItem(){ stringValue = "360p" },
new StringItem(){ stringValue = "240p" },
new StringItem(){ stringValue = "worst" },
};
private void UpdateSubAndDubString(){ private void UpdateSubAndDubString(){
HistorySeriesSoftSubsOverride.Clear(); HistorySeriesSoftSubsOverride.Clear();
HistorySeriesDubLangOverride.Clear(); HistorySeriesDubLangOverride.Clear();
@ -136,16 +172,6 @@ public class HistorySeries : INotifyPropertyChanged{
UpdateSubAndDubString(); UpdateSubAndDubString();
} }
[JsonIgnore]
public ObservableCollection<StringItem> DubLangList{ get; } = new(){
};
[JsonIgnore]
public ObservableCollection<StringItem> SubLangList{ get; } = new(){
new StringItem(){ stringValue = "all" },
new StringItem(){ stringValue = "none" },
};
public void Init(){ public void Init(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){ if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
foreach (var languageItem in Languages.languages){ foreach (var languageItem in Languages.languages){
@ -154,6 +180,8 @@ public class HistorySeries : INotifyPropertyChanged{
} }
} }
SelectedVideoQualityItem = VideoQualityList.FirstOrDefault(a => HistorySeriesVideoQualityOverride.Equals(a.stringValue)) ?? new StringItem(){ stringValue = "" };
var softSubLang = SubLangList.Where(a => HistorySeriesSoftSubsOverride.Contains(a.stringValue)).ToList(); var softSubLang = SubLangList.Where(a => HistorySeriesSoftSubsOverride.Contains(a.stringValue)).ToList();
var dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList(); var dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList();
@ -181,11 +209,7 @@ public class HistorySeries : INotifyPropertyChanged{
return; return;
try{ try{
using var client = new HttpClient(); ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl);
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
ThumbnailImage = new Bitmap(stream);
IsImageLoaded = true; IsImageLoaded = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
@ -320,10 +344,15 @@ public class HistorySeries : INotifyPropertyChanged{
Console.WriteLine($"Fetching Data for: {SeriesTitle}"); Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true; FetchingData = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));
await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId); try{
await CrunchyrollManager.Instance.History.CRUpdateSeries(SeriesId, seasonId);
} catch (Exception e){
Console.Error.WriteLine("Failed to update History series");
Console.Error.WriteLine(e);
}
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesTitle))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesTitle)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesDescription)));
// CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, this);
UpdateNewEpisodes(); UpdateNewEpisodes();
FetchingData = false; FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));

View file

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Views; using CRD.Views;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -130,4 +131,14 @@ public class NavigationMessage{
Back = back; Back = back;
Refresh = refresh; Refresh = refresh;
} }
}
public partial class SeasonViewModel : ObservableObject{
[ObservableProperty]
private bool _isSelected;
public string Season{ get; set; }
public int Year{ get; set; }
public string Display => $"{Season}\n{Year}";
} }

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI; namespace CRD.Utils.UI;

View file

@ -1,7 +1,6 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI; namespace CRD.Utils.UI;

View file

@ -1,9 +1,7 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI; namespace CRD.Utils.UI;

View file

@ -1,15 +1,20 @@
using System; using System;
using System.Globalization; using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Media; using Avalonia.Media;
using FluentAvalonia.UI.Controls; using Avalonia.Styling;
namespace CRD.Utils.UI; namespace CRD.Utils.UI;
public class UiValueConverterCalendarBackground : IValueConverter{ public class UiValueConverterCalendarBackground : IValueConverter{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture){ public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture){
if (value is bool boolValue){ if (value is bool boolValue){
return boolValue ? new SolidColorBrush(Color.Parse("#10f5d800")) : new SolidColorBrush(Color.Parse("#10FFFFFF")); var currentThemeVariant = Application.Current?.RequestedThemeVariant;
return boolValue ? currentThemeVariant == ThemeVariant.Dark ? new SolidColorBrush(Color.Parse("#583819")) : new SolidColorBrush(Color.Parse("#ffd8a1")) :
currentThemeVariant == ThemeVariant.Dark ? new SolidColorBrush(Color.Parse("#353535")) : new SolidColorBrush(Color.Parse("#d7d7d7"));
// return boolValue ? new SolidColorBrush(Color.Parse("#10f5d800")) : new SolidColorBrush(Color.Parse("#10FFFFFF"));
} }
return new SolidColorBrush(Color.Parse("#10FFFFFF")); return new SolidColorBrush(Color.Parse("#10FFFFFF"));

View file

@ -1,24 +1,25 @@
using System; using System;
using System.Collections.Generic;
using System.ComponentModel; using System.ComponentModel;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Reflection; using System.Reflection;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CRD.Utils.Updater; namespace CRD.Utils.Updater;
public class Updater : INotifyPropertyChanged{ public class Updater : INotifyPropertyChanged{
public double progress = 0; public double progress = 0;
#region Singelton #region Singelton
private static Updater? _instance; private static Updater? _instance;
private static readonly object Padlock = new(); private static readonly object Padlock = new();
public static Updater Instance{ public static Updater Instance{
get{ get{
if (_instance == null){ if (_instance == null){
@ -49,20 +50,53 @@ public class Updater : INotifyPropertyChanged{
public async Task<bool> CheckForUpdatesAsync(){ public async Task<bool> CheckForUpdatesAsync(){
try{ try{
var platformAssetMapping = new Dictionary<OSPlatform, string>{
{ OSPlatform.Windows, "windows" },
{ OSPlatform.Linux, "linux" },
{ OSPlatform.OSX, "macos" }
};
//windows-x64 windows-arm64
//linux-x64 linux-arm64
//macos-x64 macos-arm64
string platformName = platformAssetMapping.FirstOrDefault(p => RuntimeInformation.IsOSPlatform(p.Key)).Value;
string architecture = RuntimeInformation.OSArchitecture switch{
Architecture.X64 => "x64",
Architecture.Arm64 => "arm64",
_ => ""
};
platformName = $"{platformName}-{architecture}";
Console.WriteLine($"Running on {platformName}");
HttpClientHandler handler = new HttpClientHandler(); HttpClientHandler handler = new HttpClientHandler();
handler.UseProxy = false; handler.UseProxy = false;
using (var client = new HttpClient(handler)){ using (var client = new HttpClient(handler)){
client.DefaultRequestHeaders.Add("User-Agent", "C# App"); client.DefaultRequestHeaders.Add("User-Agent", "C# App");
var response = await client.GetStringAsync(apiEndpoint); var response = await client.GetStringAsync(apiEndpoint);
var releaseInfo = Helpers.Deserialize<dynamic>(response,null); var releaseInfo = Helpers.Deserialize<dynamic>(response, null);
var latestVersion = releaseInfo.tag_name; var latestVersion = releaseInfo.tag_name;
downloadUrl = releaseInfo.assets[0].browser_download_url;
foreach (var asset in releaseInfo.assets){
string assetName = (string)asset.name;
if (assetName.Contains(platformName)){
downloadUrl = asset.browser_download_url;
break;
}
}
if (string.IsNullOrEmpty(downloadUrl)){
Console.WriteLine($"Failed to get Update url for {platformName}");
return false;
}
var version = Assembly.GetExecutingAssembly().GetName().Version; var version = Assembly.GetExecutingAssembly().GetName().Version;
var currentVersion = $"v{version?.Major}.{version?.Minor}.{version?.Build}"; var currentVersion = $"v{version?.Major}.{version?.Minor}.{version?.Build}";
if (latestVersion != currentVersion){ if (latestVersion != currentVersion){
Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion); Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion);
return true; return true;
@ -80,43 +114,45 @@ public class Updater : INotifyPropertyChanged{
public async Task DownloadAndUpdateAsync(){ public async Task DownloadAndUpdateAsync(){
try{ try{
using (var client = new HttpClient()){ // Download the zip file
// Download the zip file var response = await HttpClientReq.Instance.GetHttpClient().GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode){ if (response.IsSuccessStatusCode){
var totalBytes = response.Content.Headers.ContentLength ?? -1L; var totalBytes = response.Content.Headers.ContentLength ?? -1L;
var totalBytesRead = 0L; var totalBytesRead = 0L;
var buffer = new byte[8192]; var buffer = new byte[8192];
var isMoreToRead = true; var isMoreToRead = true;
using (var stream = await response.Content.ReadAsStreamAsync()) using (var stream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)){ using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)){
do{ do{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length); var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0){ if (bytesRead == 0){
isMoreToRead = false; isMoreToRead = false;
progress = 100; progress = 100;
OnPropertyChanged(nameof(progress)); OnPropertyChanged(nameof(progress));
continue; continue;
} }
await fileStream.WriteAsync(buffer, 0, bytesRead); await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead; totalBytesRead += bytesRead;
if (totalBytes != -1){ if (totalBytes != -1){
progress = (double)totalBytesRead / totalBytes * 100; progress = (double)totalBytesRead / totalBytes * 100;
OnPropertyChanged(nameof(progress)); OnPropertyChanged(nameof(progress));
} }
} while (isMoreToRead); } while (isMoreToRead);
}
ZipFile.ExtractToDirectory(tempPath, extractPath, true);
ApplyUpdate(extractPath);
} else{
Console.Error.WriteLine("Failed to get Update");
} }
if (Directory.Exists(extractPath)){
Directory.Delete(extractPath, true);
}
ZipFile.ExtractToDirectory(tempPath, extractPath, true);
ApplyUpdate(extractPath);
} else{
Console.Error.WriteLine("Failed to get Update");
} }
} catch (Exception e){ } catch (Exception e){
Console.Error.WriteLine($"Failed to get Update: {e.Message}"); Console.Error.WriteLine($"Failed to get Update: {e.Message}");

View file

@ -1,14 +1,12 @@
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs; using CRD.Utils;
using CRD.Views.Utils; using CRD.Views.Utils;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -61,9 +59,10 @@ public partial class AccountPageViewModel : ViewModelBase{
} }
public void UpdatetProfile(){ public void UpdatetProfile(){
ProfileName = CrunchyrollManager.Instance.Profile.Username ?? "???"; // Default or fetched user name ProfileName = CrunchyrollManager.Instance.Profile.Username ?? CrunchyrollManager.Instance.Profile.ProfileName ?? "???"; // Default or fetched user name
LoginLogoutText = CrunchyrollManager.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state LoginLogoutText = CrunchyrollManager.Instance.Profile.Username == "???" ? "Login" : "Logout"; // Default state
LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" + CrunchyrollManager.Instance.Profile.Avatar); LoadProfileImage("https://static.crunchyroll.com/assets/avatar/170x170/" +
(string.IsNullOrEmpty(CrunchyrollManager.Instance.Profile.Avatar) ? "crbrand_avatars_logo_marks_mangagirl_taupe.png" : CrunchyrollManager.Instance.Profile.Avatar));
var subscriptions = CrunchyrollManager.Instance.Profile.Subscription; var subscriptions = CrunchyrollManager.Instance.Profile.Subscription;
@ -139,13 +138,7 @@ public partial class AccountPageViewModel : ViewModelBase{
public async void LoadProfileImage(string imageUrl){ public async void LoadProfileImage(string imageUrl){
try{ try{
using (var client = new HttpClient()){ ProfileImage = await Helpers.LoadImage(imageUrl);
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ProfileImage = new Bitmap(stream);
}
}
} catch (Exception ex){ } catch (Exception ex){
// Handle exceptions // Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message); Console.Error.WriteLine("Failed to load image: " + ex.Message);

View file

@ -16,6 +16,7 @@ using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music; using CRD.Utils.Structs.Crunchyroll.Music;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -56,7 +57,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
public ObservableCollection<ItemModel> Items{ get; set; } = new(); public ObservableCollection<ItemModel> Items{ get; set; } = new();
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new(); public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; set;} = new(); public ObservableCollection<ItemModel> SelectedItems{ get; set; } = new();
[ObservableProperty] [ObservableProperty]
public CrBrowseSeries _selectedSearchItem; public CrBrowseSeries _selectedSearchItem;
@ -64,7 +65,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
public ComboBoxItem _currentSelectedSeason; public ComboBoxItem _currentSelectedSeason;
public ObservableCollection<ComboBoxItem> SeasonList{ get;set; } = new(); public ObservableCollection<ComboBoxItem> SeasonList{ get; set; } = new();
private Dictionary<string, List<ItemModel>> episodesBySeason = new(); private Dictionary<string, List<ItemModel>> episodesBySeason = new();
@ -75,7 +76,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private CrunchyMusicVideoList? currentMusicVideoList; private CrunchyMusicVideoList? currentMusicVideoList;
private bool CurrentSeasonFullySelected = false; private bool CurrentSeasonFullySelected = false;
public AddDownloadPageViewModel(){ public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged; SelectedItems.CollectionChanged += OnSelectedItemsChanged;
} }
@ -200,7 +201,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (music != null){ if (music != null){
var meta = musicClass.EpisodeMeta(music); var meta = musicClass.EpisodeMeta(music);
QueueManager.Instance.CrAddEpMetaToQueue(meta); QueueManager.Instance.CrAddMusicMetaToQueue(meta);
} }
} }
} }
@ -243,11 +244,10 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false; ButtonEnabled = false;
SearchVisible = true; SearchVisible = true;
SlectSeasonVisible = false; SlectSeasonVisible = false;
//TODO - find a better way to reduce ram usage //TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); GC.Collect();
} }
private async Task HandleUrlInputAsync(){ private async Task HandleUrlInputAsync(){
@ -426,7 +426,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){ partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){
if(SelectedItems == null) return; if (SelectedItems == null) return;
foreach (var selectedItem in SelectedItems){ foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){ if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum); selectedEpisodes.Add(selectedItem.AbsolutNum);
@ -443,8 +443,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){ private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
if(Items == null) return; if (Items == null) return;
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item)); CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){ if (CurrentSeasonFullySelected){
@ -523,7 +523,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
SelectedItems.Clear(); SelectedItems.Clear();
episodesBySeason.Clear(); episodesBySeason.Clear();
SeasonList.Clear(); SeasonList.Clear();
//TODO - find a better way to reduce ram usage //TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce; GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect(); GC.Collect();
@ -597,28 +597,25 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
} }
public void Dispose(){ 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
}
foreach (var itemModel in Items){ // Clear collections and other managed resources
itemModel.ImageBitmap?.Dispose(); // Dispose the bitmap if it exists Items.Clear();
itemModel.ImageBitmap = null; // Nullify the reference to avoid lingering references Items = null;
} SearchItems.Clear();
SearchItems = null;
// Clear collections and other managed resources SelectedItems.Clear();
Items.Clear(); SelectedItems = null;
Items = null; SeasonList.Clear();
SearchItems.Clear(); SeasonList = null;
SearchItems = null; episodesBySeason.Clear();
SelectedItems.Clear(); episodesBySeason = null;
SelectedItems = null; selectedEpisodes.Clear();
SeasonList.Clear(); selectedEpisodes = null;
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{ public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
@ -640,7 +637,7 @@ public class ItemModel(string id, string imageUrl, string description, string ti
public event PropertyChangedEventHandler? PropertyChanged; public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){ public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url,208,117); ImageBitmap = await Helpers.LoadImage(url, 208, 117);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap))); PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
} }
} }

View file

@ -1,27 +1,33 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Reflection; using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using CRD.Downloader; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData; using DynamicData;
using DynamicData.Kernel;
using Newtonsoft.Json;
namespace CRD.ViewModels; namespace CRD.ViewModels;
public partial class CalendarPageViewModel : ViewModelBase{ public partial class CalendarPageViewModel : ViewModelBase{
public ObservableCollection<CalendarDay> CalendarDays{ get; set; } public ObservableCollection<CalendarDay> CalendarDays{ get; set; }
[ObservableProperty]
private bool _prevButtonEnabled = true;
[ObservableProperty]
private bool _nextButtonEnabled = true;
[ObservableProperty] [ObservableProperty]
private bool _showLoading; private bool _showLoading;
@ -62,9 +68,8 @@ public partial class CalendarPageViewModel : ViewModelBase{
private CalendarWeek? currentWeek; private CalendarWeek? currentWeek;
private bool loading = true; private bool loading = true;
public CalendarPageViewModel(){
public CalendarPageViewModel(){
CalendarDays = new ObservableCollection<CalendarDay>(); CalendarDays = new ObservableCollection<CalendarDay>();
foreach (var languageItem in Languages.languages){ foreach (var languageItem in Languages.languages){
@ -80,39 +85,46 @@ public partial class CalendarPageViewModel : ViewModelBase{
CurrentCalendarLanguage = CalendarLanguage.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage) ?? CalendarLanguage[0]; CurrentCalendarLanguage = CalendarLanguage.FirstOrDefault(a => a.Content != null && (string)a.Content == CrunchyrollManager.Instance.CrunOptions.SelectedCalendarLanguage) ?? CalendarLanguage[0];
loading = false; loading = false;
LoadCalendar(GetThisWeeksMondayDate(), false); LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, false);
} }
private string GetThisWeeksMondayDate(){ private string GetThisWeeksMondayDate(){
// Get today's date
DateTime today = DateTime.Today; DateTime today = DateTime.Today;
// Calculate the number of days to subtract to get to Monday
// DayOfWeek.Monday is 1, so if today is Monday, subtract 0 days, if it's Tuesday subtract 1 day, etc.
int daysToSubtract = (int)today.DayOfWeek - (int)DayOfWeek.Monday; int daysToSubtract = (int)today.DayOfWeek - (int)DayOfWeek.Monday;
// If today is Sunday (0), it will subtract -1, which we need to adjust to 6 to go back to the previous Monday
if (daysToSubtract < 0){ if (daysToSubtract < 0){
daysToSubtract += 7; daysToSubtract += 7;
} }
// Get the date of the most recent Monday
DateTime monday = today.AddDays(-daysToSubtract); DateTime monday = today.AddDays(-daysToSubtract);
// Format and print the date
string formattedDate = monday.ToString("yyyy-MM-dd"); string formattedDate = monday.ToString("yyyy-MM-dd");
return formattedDate; return formattedDate;
} }
public async void LoadCalendar(string mondayDate, bool forceUpdate){ public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){
ShowLoading = true; ShowLoading = true;
CalendarWeek week; CalendarWeek week;
if (CustomCalendar){ if (CustomCalendar){
week = await CalendarManager.Instance.BuildCustomCalendar(forceUpdate);
if (customCalDate.Date == DateTime.Now.Date){
PrevButtonEnabled = false;
NextButtonEnabled = true;
} else{
PrevButtonEnabled = true;
NextButtonEnabled = false;
}
week = await CalendarManager.Instance.BuildCustomCalendar(customCalDate, forceUpdate);
} else{ } else{
PrevButtonEnabled = true;
NextButtonEnabled = true;
week = await CalendarManager.Instance.GetCalendarForDate(mondayDate, forceUpdate); week = await CalendarManager.Instance.GetCalendarForDate(mondayDate, forceUpdate);
if (currentWeek != null && currentWeek == week){ if (currentWeek != null && currentWeek == week){
ShowLoading = false; ShowLoading = false;
@ -129,7 +141,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var calendarDay in CalendarDays){ foreach (var calendarDay in CalendarDays){
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){ foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.ImageBitmap == null){
calendarDayCalendarEpisode.LoadImage(); if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150);
} else{
calendarDayCalendarEpisode.LoadImage();
}
} }
} }
} }
@ -143,7 +160,11 @@ public partial class CalendarPageViewModel : ViewModelBase{
} }
if (calendarDayCalendarEpisode.ImageBitmap == null){ if (calendarDayCalendarEpisode.ImageBitmap == null){
calendarDayCalendarEpisode.LoadImage(); if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150);
} else{
calendarDayCalendarEpisode.LoadImage();
}
} }
} }
} }
@ -175,7 +196,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
mondayDate = GetThisWeeksMondayDate(); mondayDate = GetThisWeeksMondayDate();
} }
LoadCalendar(mondayDate, true); var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
}
LoadCalendar(mondayDate,refreshDate, true);
} }
[RelayCommand] [RelayCommand]
@ -186,13 +212,18 @@ public partial class CalendarPageViewModel : ViewModelBase{
string mondayDate; string mondayDate;
if (currentWeek is{ FirstDayOfWeek: not null }){ if (currentWeek is{ FirstDayOfWeek: var firstDay } && firstDay != DateTime.MinValue){
mondayDate = PreviousMonday((DateTime)currentWeek.FirstDayOfWeek); mondayDate = PreviousMonday(currentWeek.FirstDayOfWeek);
} else{ } else{
mondayDate = GetThisWeeksMondayDate(); mondayDate = GetThisWeeksMondayDate();
} }
var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
}
LoadCalendar(mondayDate, false); LoadCalendar(mondayDate,refreshDate, false);
} }
[RelayCommand] [RelayCommand]
@ -203,13 +234,20 @@ public partial class CalendarPageViewModel : ViewModelBase{
string mondayDate; string mondayDate;
if (currentWeek is{ FirstDayOfWeek: not null }){ if (currentWeek is{ FirstDayOfWeek: var firstDay } && firstDay != DateTime.MinValue){
mondayDate = NextMonday((DateTime)currentWeek.FirstDayOfWeek); mondayDate = NextMonday(currentWeek.FirstDayOfWeek);
} else{ } else{
mondayDate = GetThisWeeksMondayDate(); mondayDate = GetThisWeeksMondayDate();
} }
var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(13);
}
LoadCalendar(mondayDate, false); LoadCalendar(mondayDate,refreshDate, false);
} }
@ -232,7 +270,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value; CrunchyrollManager.Instance.CrunOptions.CustomCalendar = value;
LoadCalendar(GetThisWeeksMondayDate(), true); LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
CfgManager.WriteSettingsToFile(); CfgManager.WriteSettingsToFile();
} }
@ -265,4 +303,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteSettingsToFile(); CfgManager.WriteSettingsToFile();
} }
} }
} }

View file

@ -1,8 +1,8 @@
using System; using System;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Net.Http;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -59,7 +59,6 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
public string DoingWhat{ get; set; } public string DoingWhat{ get; set; }
public string DownloadSpeed{ get; set; } public string DownloadSpeed{ get; set; }
public string InfoText{ get; set; } public string InfoText{ get; set; }
public CrunchyEpMeta epMeta{ get; set; } public CrunchyEpMeta epMeta{ get; set; }
@ -78,9 +77,11 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
Time = "Estimated Time: " + TimeSpan.FromSeconds(epMeta.DownloadProgress.Time).ToString(@"hh\:mm\:ss"); Time = "Estimated Time: " + TimeSpan.FromSeconds(epMeta.DownloadProgress.Time).ToString(@"hh\:mm\:ss");
DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s"; DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s";
Paused = epMeta.Paused || !isDownloading && !epMeta.Paused; Paused = epMeta.Paused || !isDownloading && !epMeta.Paused;
DoingWhat = epMeta.Paused ? "Paused" : Done ? "Done" : epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; DoingWhat = epMeta.Paused ? "Paused" :
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString(); if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : "");
Error = epMeta.DownloadProgress.Error; Error = epMeta.DownloadProgress.Error;
} }
@ -94,9 +95,9 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
} }
private string GetSubtitleString(){ private string GetSubtitleString(){
var hardSubs = CrunchyrollManager.Instance.CrunOptions.Hslang != "none" ? "Hardsub: " : ""; var hardSubs = epMeta.Hslang != "none" ? "Hardsub: " : "";
if (hardSubs != string.Empty){ if (hardSubs != string.Empty){
var locale = Languages.Locale2language(CrunchyrollManager.Instance.CrunOptions.Hslang).CrLocale; var locale = Languages.Locale2language(epMeta.Hslang).CrLocale;
if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(locale)){ if (epMeta.AvailableSubs != null && epMeta.AvailableSubs.Contains(locale)){
hardSubs += locale + " "; hardSubs += locale + " ";
} }
@ -133,9 +134,11 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s"; DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s";
Paused = epMeta.Paused || !isDownloading && !epMeta.Paused; Paused = epMeta.Paused || !isDownloading && !epMeta.Paused;
DoingWhat = epMeta.Paused ? "Paused" : Done ? "Done" : epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting"; DoingWhat = epMeta.Paused ? "Paused" :
Done ? (epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Done") :
epMeta.DownloadProgress.Doing != string.Empty ? epMeta.DownloadProgress.Doing : "Waiting";
if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString(); if (epMeta.Data != null) InfoText = GetDubString() + " - " + GetSubtitleString() + (!string.IsNullOrEmpty(epMeta.Resolution) ? "- " + epMeta.Resolution : "");
Error = epMeta.DownloadProgress.Error; Error = epMeta.DownloadProgress.Error;
@ -192,6 +195,16 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null; CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
if (downloadItem != null){ if (downloadItem != null){
QueueManager.Instance.Queue.Remove(downloadItem); QueueManager.Instance.Queue.Remove(downloadItem);
if (!Done){
foreach (var downloadItemDownloadedFile in downloadItem.downloadedFiles){
try{
if (File.Exists(downloadItemDownloadedFile)){
File.Delete(downloadItemDownloadedFile);
}
} catch (Exception e){
}
}
}
} }
} }

View file

@ -2,10 +2,7 @@ using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq; using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
@ -19,9 +16,7 @@ using CRD.Utils;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData; using DynamicData;
using HarfBuzzSharp;
using ReactiveUI; using ReactiveUI;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -29,10 +24,10 @@ namespace CRD.ViewModels;
public partial class HistoryPageViewModel : ViewModelBase{ public partial class HistoryPageViewModel : ViewModelBase{
public ObservableCollection<HistorySeries> Items{ get; } public ObservableCollection<HistorySeries> Items{ get; }
public ObservableCollection<HistorySeries> FilteredItems{ get; } public ObservableCollection<HistorySeries> FilteredItems{ get; }
[ObservableProperty] [ObservableProperty]
private ProgramManager _programManager; private ProgramManager _programManager;
[ObservableProperty] [ObservableProperty]
private HistorySeries _selectedSeries; private HistorySeries _selectedSeries;
@ -107,14 +102,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty] [ObservableProperty]
private static bool _sonarrAvailable; private static bool _sonarrAvailable;
[ObservableProperty] [ObservableProperty]
private static string _progressText; private static string _progressText;
public HistoryPageViewModel(){ public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance; ProgramManager = ProgramManager.Instance;
_storageProvider = ProgramManager.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.StorageProvider));
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){ if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled; SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
} else{ } else{
@ -149,11 +145,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))){ foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))){
if (!SonarrAvailable && (filterType == FilterType.MissingEpisodesSonarr || filterType == FilterType.ContinuingOnly)){ if (!SonarrAvailable && (filterType == FilterType.MissingEpisodesSonarr || filterType == FilterType.ContinuingOnly)){
continue; continue;
} }
var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType }; var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
FilterList.Add(item); FilterList.Add(item);
if (filterType == currentFilterType){ if (filterType == currentFilterType){
@ -224,7 +219,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (SelectedFilter != null){ if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter); OnSelectedFilterChanged(SelectedFilter);
} }
} else{ } else{
Console.Error.WriteLine("Invalid viewtype selected"); Console.Error.WriteLine("Invalid viewtype selected");
} }
@ -235,11 +229,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
partial void OnSelectedFilterChanged(FilterListElement? value){ partial void OnSelectedFilterChanged(FilterListElement? value){
if (value == null){ if (value == null){
return; return;
} }
currentFilterType = value.SelectedType; currentFilterType = value.SelectedType;
if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType; if (CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties != null) CrunchyrollManager.Instance.CrunOptions.HistoryPageProperties.SelectedFilter = currentFilterType;
@ -293,7 +286,11 @@ public partial class HistoryPageViewModel : ViewModelBase{
} }
partial void OnSelectedSeriesChanged(HistorySeries value){ partial void OnSelectedSeriesChanged(HistorySeries? value){
if (value == null){
return;
}
CrunchyrollManager.Instance.SelectedSeries = value; CrunchyrollManager.Instance.SelectedSeries = value;
NavToSeries(); NavToSeries();
@ -399,17 +396,17 @@ public partial class HistoryPageViewModel : ViewModelBase{
Console.Error.WriteLine($"[Sonarr Match] {series.Title} already matched"); Console.Error.WriteLine($"[Sonarr Match] {series.Title} already matched");
} }
}); });
var seriesIds = concurrentSeriesIds.ToList(); var seriesIds = concurrentSeriesIds.ToList();
var totalSeries = seriesIds.Count; var totalSeries = seriesIds.Count;
for (int count = 0; count < totalSeries; count++){ for (int count = 0; count < totalSeries; count++){
ProgressText = $"{count + 1}/{totalSeries}"; ProgressText = $"{count + 1}/{totalSeries}";
// Await the CRUpdateSeries task for each seriesId // Await the CRUpdateSeries task for each seriesId
await crInstance.History.CRUpdateSeries(seriesIds[count], ""); await crInstance.History.CRUpdateSeries(seriesIds[count], "");
} }
// var updateTasks = seriesIds.Select(seriesId => crInstance.History.CRUpdateSeries(seriesId, "")); // var updateTasks = seriesIds.Select(seriesId => crInstance.History.CRUpdateSeries(seriesId, ""));
// await Task.WhenAll(updateTasks); // await Task.WhenAll(updateTasks);
} }
@ -495,11 +492,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
await Task.WhenAll(downloadTasks); await Task.WhenAll(downloadTasks);
} }
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
} }
public class HistoryPageProperties(){ public class HistoryPageProperties(){

View file

@ -1,11 +1,8 @@
using System; using System;
using System.Collections.ObjectModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Shapes;
using Avalonia.Platform.Storage; using Avalonia.Platform.Storage;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@ -13,7 +10,6 @@ using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs; using CRD.Utils.Structs;
using CRD.Utils.Structs.History; using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils; using CRD.ViewModels.Utils;
@ -21,7 +17,6 @@ using CRD.Views;
using CRD.Views.Utils; using CRD.Views.Utils;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using ReactiveUI; using ReactiveUI;
using Path = Avalonia.Controls.Shapes.Path;
namespace CRD.ViewModels; namespace CRD.ViewModels;
@ -53,6 +48,9 @@ public partial class SeriesPageViewModel : ViewModelBase{
public bool _seriesFolderPathExists; public bool _seriesFolderPathExists;
public SeriesPageViewModel(){ public SeriesPageViewModel(){
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
_selectedSeries = CrunchyrollManager.Instance.SelectedSeries; _selectedSeries = CrunchyrollManager.Instance.SelectedSeries;
if (_selectedSeries.ThumbnailImage == null){ if (_selectedSeries.ThumbnailImage == null){
@ -155,11 +153,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
UpdateSeriesFolderPath(); UpdateSeriesFolderPath();
} }
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
[RelayCommand] [RelayCommand]
public async Task MatchSonarrSeries_Button(){ public async Task MatchSonarrSeries_Button(){
var dialog = new ContentDialog(){ var dialog = new ContentDialog(){

View file

@ -1,904 +1,54 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Media; using Avalonia.Layout;
using Avalonia.Platform.Storage; using Avalonia.Media.Imaging;
using Avalonia.Styling; using CRD.Downloader.Crunchyroll.ViewModels;
using CommunityToolkit.Mvvm.ComponentModel; using CRD.Downloader.Crunchyroll.Views;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils; using CRD.ViewModels.Utils;
using CRD.Views.Utils; using CRD.Views.Utils;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Image = Avalonia.Controls.Image;
// ReSharper disable InconsistentNaming // ReSharper disable InconsistentNaming
namespace CRD.ViewModels; namespace CRD.ViewModels;
public partial class SettingsPageViewModel : ViewModelBase{ public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private string _currentVersion; public ObservableCollection<TabViewItem> Tabs{ get; } = new();
[ObservableProperty] private TabViewItem CreateTab(string header, string iconPath, UserControl content, object viewModel){
private bool _downloadVideo = true; content.DataContext = viewModel;
[ObservableProperty] Bitmap bitmap = null;
private bool _downloadAudio = true; try{
// Load the image using AssetLoader.Open
[ObservableProperty] bitmap = new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(iconPath)));
private bool _downloadChapters = true; } catch (Exception ex){
Console.WriteLine($"Error loading image: {ex.Message}");
[ObservableProperty] }
private bool _addScaledBorderAndShadow;
return new TabViewItem{
[ObservableProperty] Header = new StackPanel{
private bool _includeSignSubs; Orientation = Orientation.Horizontal,
Spacing = 5,
[ObservableProperty] Children ={
private bool _includeCcSubs; new Image{ Source = bitmap, Width = 18, Height = 18 },
new TextBlock{ Text = header, FontSize = 16}
[ObservableProperty] }
private bool _downloadToTempFolder; },
IsClosable = false,
[ObservableProperty] Content = content
private ComboBoxItem _selectedScaledBorderAndShadow; };
}
public ObservableCollection<ComboBoxItem> ScaledBorderAndShadow{ get; } = new(){
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: yes" },
new ComboBoxItem(){ Content = "ScaledBorderAndShadow: no" },
};
[ObservableProperty]
private bool _skipMuxing;
[ObservableProperty]
private bool _muxToMp4;
[ObservableProperty]
private bool _syncTimings;
[ObservableProperty]
private bool _defaultSubSigns;
[ObservableProperty]
private bool _defaultSubForcedDisplay;
[ObservableProperty]
private bool _includeEpisodeDescription;
[ObservableProperty]
private bool _downloadVideoForEveryDub;
[ObservableProperty]
private bool _keepDubsSeparate;
[ObservableProperty]
private bool _skipSubMux;
[ObservableProperty]
private bool _history;
[ObservableProperty]
private bool _historyAddSpecials;
[ObservableProperty]
private bool _historyCountSonarr;
[ObservableProperty]
private double? _leadingNumbers;
[ObservableProperty]
private double? _simultaneousDownloads;
[ObservableProperty]
private double? _downloadSpeed;
[ObservableProperty]
private string _fileName = "";
[ObservableProperty]
private string _fileTitle = "";
[ObservableProperty]
private ObservableCollection<StringItem> _mkvMergeOptions = new();
[ObservableProperty]
private string _mkvMergeOption = "";
[ObservableProperty]
private string _ffmpegOption = "";
[ObservableProperty]
private ObservableCollection<StringItem> _ffmpegOptions = new();
[ObservableProperty]
private string _selectedSubs = "all";
[ObservableProperty]
private ComboBoxItem _selectedHSLang;
[ObservableProperty]
private ComboBoxItem _selectedHistoryLang;
[ObservableProperty]
private ComboBoxItem _selectedDescriptionLang;
[ObservableProperty]
private string _selectedDubs = "ja-JP";
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedDubLang = new();
[ObservableProperty]
private ComboBoxItem _selectedStreamEndpoint;
[ObservableProperty]
private ComboBoxItem _selectedDefaultDubLang;
[ObservableProperty]
private ComboBoxItem _selectedDefaultSubLang;
[ObservableProperty]
private ComboBoxItem? _selectedVideoQuality;
[ObservableProperty]
private ComboBoxItem? _selectedAudioQuality;
[ObservableProperty]
private ComboBoxItem? _currentAppTheme;
[ObservableProperty]
private ObservableCollection<ListBoxItem> _selectedSubLang = new();
[ObservableProperty]
private bool _useCustomAccent;
[ObservableProperty]
private Color _listBoxColor;
[ObservableProperty]
private Color _customAccentColor = Colors.SlateBlue;
[ObservableProperty]
private string _sonarrHost = "localhost";
[ObservableProperty]
private string _sonarrPort = "8989";
[ObservableProperty]
private string _sonarrApiKey = "";
[ObservableProperty]
private bool _sonarrUseSsl = false;
[ObservableProperty]
private bool _sonarrUseSonarrNumbering = false;
[ObservableProperty]
private bool _logMode = false;
public ObservableCollection<Color> PredefinedColors{ get; } = new(){
Color.FromRgb(255, 185, 0),
Color.FromRgb(255, 140, 0),
Color.FromRgb(247, 99, 12),
Color.FromRgb(202, 80, 16),
Color.FromRgb(218, 59, 1),
Color.FromRgb(239, 105, 80),
Color.FromRgb(209, 52, 56),
Color.FromRgb(255, 67, 67),
Color.FromRgb(231, 72, 86),
Color.FromRgb(232, 17, 35),
Color.FromRgb(234, 0, 94),
Color.FromRgb(195, 0, 82),
Color.FromRgb(227, 0, 140),
Color.FromRgb(191, 0, 119),
Color.FromRgb(194, 57, 179),
Color.FromRgb(154, 0, 137),
Color.FromRgb(0, 120, 212),
Color.FromRgb(0, 99, 177),
Color.FromRgb(142, 140, 216),
Color.FromRgb(107, 105, 214),
Colors.SlateBlue,
Color.FromRgb(135, 100, 184),
Color.FromRgb(116, 77, 169),
Color.FromRgb(177, 70, 194),
Color.FromRgb(136, 23, 152),
Color.FromRgb(0, 153, 188),
Color.FromRgb(45, 125, 154),
Color.FromRgb(0, 183, 195),
Color.FromRgb(3, 131, 135),
Color.FromRgb(0, 178, 148),
Color.FromRgb(1, 133, 116),
Color.FromRgb(0, 204, 106),
Color.FromRgb(16, 137, 62),
Color.FromRgb(122, 117, 116),
Color.FromRgb(93, 90, 88),
Color.FromRgb(104, 118, 138),
Color.FromRgb(81, 92, 107),
Color.FromRgb(86, 124, 115),
Color.FromRgb(72, 104, 96),
Color.FromRgb(73, 130, 5),
Color.FromRgb(16, 124, 16),
Color.FromRgb(118, 118, 118),
Color.FromRgb(76, 74, 72),
Color.FromRgb(105, 121, 126),
Color.FromRgb(74, 84, 89),
Color.FromRgb(100, 124, 100),
Color.FromRgb(82, 94, 84),
Color.FromRgb(132, 117, 69),
Color.FromRgb(126, 115, 95)
};
public ObservableCollection<ComboBoxItem> AppThemes{ get; } = new(){
new ComboBoxItem(){ Content = "System" },
new ComboBoxItem(){ Content = "Light" },
new ComboBoxItem(){ Content = "Dark" },
};
public ObservableCollection<ComboBoxItem> VideoQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "1080" },
new ComboBoxItem(){ Content = "720" },
new ComboBoxItem(){ Content = "480" },
new ComboBoxItem(){ Content = "360" },
new ComboBoxItem(){ Content = "240" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> AudioQualityList{ get; } = new(){
new ComboBoxItem(){ Content = "best" },
new ComboBoxItem(){ Content = "128kB/s" },
new ComboBoxItem(){ Content = "96kB/s" },
new ComboBoxItem(){ Content = "64kB/s" },
new ComboBoxItem(){ Content = "worst" },
};
public ObservableCollection<ComboBoxItem> HardSubLangList{ get; } = new(){
new ComboBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> HistoryLangList{ get; } = new(){
new ComboBoxItem(){ Content = "default" },
new ComboBoxItem(){ Content = "de-DE" },
new ComboBoxItem(){ Content = "en-US" },
new ComboBoxItem(){ Content = "es-419" },
new ComboBoxItem(){ Content = "es-ES" },
new ComboBoxItem(){ Content = "fr-FR" },
new ComboBoxItem(){ Content = "it-IT" },
new ComboBoxItem(){ Content = "pt-BR" },
new ComboBoxItem(){ Content = "pt-PT" },
new ComboBoxItem(){ Content = "ru-RU" },
new ComboBoxItem(){ Content = "hi-IN" },
new ComboBoxItem(){ Content = "ar-SA" },
};
public ObservableCollection<ComboBoxItem> DescriptionLangList{ get; } = new(){
new ComboBoxItem(){ Content = "default" },
new ComboBoxItem(){ Content = "de-DE" },
new ComboBoxItem(){ Content = "en-US" },
new ComboBoxItem(){ Content = "es-419" },
new ComboBoxItem(){ Content = "es-ES" },
new ComboBoxItem(){ Content = "fr-FR" },
new ComboBoxItem(){ Content = "it-IT" },
new ComboBoxItem(){ Content = "pt-BR" },
new ComboBoxItem(){ Content = "pt-PT" },
new ComboBoxItem(){ Content = "ru-RU" },
new ComboBoxItem(){ Content = "hi-IN" },
new ComboBoxItem(){ Content = "ar-SA" },
};
public ObservableCollection<ListBoxItem> DubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultDubLangList{ get; } = new(){
};
public ObservableCollection<ComboBoxItem> DefaultSubLangList{ get; } = new(){
};
public ObservableCollection<ListBoxItem> SubLangList{ get; } = new(){
new ListBoxItem(){ Content = "all" },
new ListBoxItem(){ Content = "none" },
};
public ObservableCollection<ComboBoxItem> StreamEndpoints{ get; } = new(){
new ComboBoxItem(){ Content = "web/firefox" },
new ComboBoxItem(){ Content = "console/switch" },
new ComboBoxItem(){ Content = "console/ps4" },
new ComboBoxItem(){ Content = "console/ps5" },
new ComboBoxItem(){ Content = "console/xbox_one" },
new ComboBoxItem(){ Content = "web/edge" },
// new ComboBoxItem(){ Content = "web/safari" },
new ComboBoxItem(){ Content = "web/chrome" },
new ComboBoxItem(){ Content = "web/fallback" },
// new ComboBoxItem(){ Content = "ios/iphone" },
// new ComboBoxItem(){ Content = "ios/ipad" },
new ComboBoxItem(){ Content = "android/phone" },
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;
private IStorageProvider _storageProvider;
public SettingsPageViewModel(){ public SettingsPageViewModel(){
var version = Assembly.GetExecutingAssembly().GetName().Version; // Add initial tabs
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}"; Tabs.Add(CreateTab("General Settings", "avares://CRD/Assets/app_icon.ico", new GeneralSettingsView(), new GeneralSettingsViewModel()));
Tabs.Add(CreateTab("Crunchyroll Settings", "avares://CRD/Assets/crunchy_icon_round.png", new CrunchyrollSettingsView(), new CrunchyrollSettingsViewModel()));
_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 });
DubLangList.Add(new ListBoxItem{ Content = languageItem.CrLocale });
DefaultDubLangList.Add(new ComboBoxItem{ Content = languageItem.CrLocale });
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];
ComboBoxItem? historyLang = HistoryLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.HistoryLang) ?? null;
SelectedHistoryLang = historyLang ?? HistoryLangList[0];
ComboBoxItem? hsLang = HardSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == Languages.Locale2language(options.Hslang).CrLocale) ?? null;
SelectedHSLang = hsLang ?? HardSubLangList[0];
ComboBoxItem? defaultDubLang = DefaultDubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultAudio ?? "")) ?? null;
SelectedDefaultDubLang = defaultDubLang ?? DefaultDubLangList[0];
ComboBoxItem? defaultSubLang = DefaultSubLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.DefaultSub ?? "")) ?? null;
SelectedDefaultSubLang = defaultSubLang ?? DefaultSubLangList[0];
ComboBoxItem? streamEndpoint = StreamEndpoints.FirstOrDefault(a => a.Content != null && (string)a.Content == (options.StreamEndpoint ?? "")) ?? null;
SelectedStreamEndpoint = streamEndpoint ?? StreamEndpoints[0];
var softSubLang = SubLangList.Where(a => options.DlSubs.Contains(a.Content)).ToList();
SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem);
}
var dubLang = DubLangList.Where(a => options.DubLang.Contains(a.Content)).ToList();
SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
var props = options.SonarrProperties;
if (props != null){
SonarrUseSsl = props.UseSsl;
SonarrUseSonarrNumbering = props.UseSonarrNumbering;
SonarrHost = props.Host + "";
SonarrPort = props.Port + "";
SonarrApiKey = props.ApiKey + "";
}
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;
HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit;
IncludeEpisodeDescription = options.IncludeVideoDescription;
FileTitle = options.VideoTitle ?? "";
IncludeSignSubs = options.IncludeSignsSubs;
IncludeCcSubs = options.IncludeCcSubs;
DownloadVideo = !options.Novids;
DownloadAudio = !options.Noaudio;
DownloadVideoForEveryDub = !options.DlVideoOnce;
DownloadToTempFolder = options.DownloadToTempFolder;
KeepDubsSeparate = options.KeepDubsSeperate;
DownloadChapters = options.Chapters;
MuxToMp4 = options.Mp4;
SyncTimings = options.SyncTiming;
SkipSubMux = options.SkipSubsMux;
LeadingNumbers = options.Numbers;
FileName = options.FileName;
SimultaneousDownloads = options.SimultaneousDownloads;
LogMode = options.LogMode;
ComboBoxItem? qualityAudio = AudioQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityAudio) ?? null;
SelectedAudioQuality = qualityAudio ?? AudioQualityList[0];
ComboBoxItem? qualityVideo = VideoQualityList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.QualityVideo) ?? null;
SelectedVideoQuality = qualityVideo ?? VideoQualityList[0];
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
CurrentAppTheme = theme ?? AppThemes[0];
if (!string.IsNullOrEmpty(options.AccentColor) && options.AccentColor != Application.Current?.PlatformSettings?.GetColorValues().AccentColor1.ToString()){
UseCustomAccent = true;
}
History = options.History;
MkvMergeOptions.Clear();
if (options.MkvmergeOptions != null){
foreach (var mkvmergeParam in options.MkvmergeOptions){
MkvMergeOptions.Add(new StringItem(){ stringValue = mkvmergeParam });
}
}
FfmpegOptions.Clear();
if (options.FfmpegOptions != null){
foreach (var ffmpegParam in options.FfmpegOptions){
FfmpegOptions.Add(new StringItem(){ stringValue = ffmpegParam });
}
}
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
MkvMergeOptions.CollectionChanged += Changes;
FfmpegOptions.CollectionChanged += Changes;
settingsLoaded = true;
} }
private void UpdateSettings(){
if (!settingsLoaded){
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;
CrunchyrollManager.Instance.CrunOptions.IncludeVideoDescription = IncludeEpisodeDescription;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr;
CrunchyrollManager.Instance.CrunOptions.VideoTitle = FileTitle;
CrunchyrollManager.Instance.CrunOptions.Novids = !DownloadVideo;
CrunchyrollManager.Instance.CrunOptions.Noaudio = !DownloadAudio;
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;
CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10);
CrunchyrollManager.Instance.CrunOptions.FileName = FileName;
CrunchyrollManager.Instance.CrunOptions.IncludeSignsSubs = IncludeSignSubs;
CrunchyrollManager.Instance.CrunOptions.IncludeCcSubs = IncludeCcSubs;
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>();
foreach (var listBoxItem in SelectedSubLang){
softSubs.Add(listBoxItem.Content + "");
}
CrunchyrollManager.Instance.CrunOptions.DlSubs = softSubs;
string descLang = SelectedDescriptionLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DescriptionLang = descLang != "default" ? descLang : CrunchyrollManager.Instance.DefaultLocale;
string historyLang = SelectedHistoryLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale;
string hslang = SelectedHSLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.Hslang = hslang != "none" ? Languages.FindLang(hslang).Locale : hslang;
CrunchyrollManager.Instance.CrunOptions.DefaultAudio = SelectedDefaultDubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.DefaultSub = SelectedDefaultSubLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.StreamEndpoint = SelectedStreamEndpoint.Content + "";
List<string> dubLangs = new List<string>();
foreach (var listBoxItem in SelectedDubLang){
dubLangs.Add(listBoxItem.Content + "");
}
CrunchyrollManager.Instance.CrunOptions.DubLang = dubLangs;
CrunchyrollManager.Instance.CrunOptions.QualityAudio = SelectedAudioQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.QualityVideo = SelectedVideoQuality?.Content + "";
CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + "";
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;
var props = new SonarrProperties();
props.UseSsl = SonarrUseSsl;
props.UseSonarrNumbering = SonarrUseSonarrNumbering;
props.Host = SonarrHost;
if (int.TryParse(SonarrPort, out var portNumber)){
props.Port = portNumber;
} else{
props.Port = 8989;
}
props.ApiKey = SonarrApiKey;
CrunchyrollManager.Instance.CrunOptions.SonarrProperties = props;
CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode;
List<string> mkvmergeParams = new List<string>();
foreach (var mkvmergeParam in MkvMergeOptions){
mkvmergeParams.Add(mkvmergeParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.MkvmergeOptions = mkvmergeParams;
List<string> ffmpegParams = new List<string>();
foreach (var ffmpegParam in FfmpegOptions){
ffmpegParams.Add(ffmpegParam.stringValue);
}
CrunchyrollManager.Instance.CrunOptions.FfmpegOptions = ffmpegParams;
CfgManager.WriteSettingsToFile();
}
private ScaledBorderAndShadowSelection GetScaledBorderAndShadowSelection(){
if (!AddScaledBorderAndShadow){
return ScaledBorderAndShadowSelection.DontAdd;
}
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: yes"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
if (SelectedScaledBorderAndShadow.Content + "" == "ScaledBorderAndShadow: no"){
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo;
}
return ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
}
private ComboBoxItem GetScaledBorderAndShadowFromOptions(CrDownloadOptions options){
switch (options.SubsAddScaledBorder){
case (ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes):
return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: yes") ?? ScaledBorderAndShadow[0];
case ScaledBorderAndShadowSelection.ScaledBorderAndShadowNo:
return ScaledBorderAndShadow.FirstOrDefault(a => a.Content != null && (string)a.Content == "ScaledBorderAndShadow: no") ?? ScaledBorderAndShadow[0];
default:
return ScaledBorderAndShadow[0];
}
}
[RelayCommand]
public void AddMkvMergeParam(){
MkvMergeOptions.Add(new StringItem(){ stringValue = MkvMergeOption });
MkvMergeOption = "";
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void RemoveMkvMergeParam(StringItem param){
MkvMergeOptions.Remove(param);
RaisePropertyChanged(nameof(MkvMergeOptions));
}
[RelayCommand]
public void AddFfmpegParam(){
FfmpegOptions.Add(new StringItem(){ stringValue = FfmpegOption });
FfmpegOption = "";
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public void RemoveFfmpegParam(StringItem param){
FfmpegOptions.Remove(param);
RaisePropertyChanged(nameof(FfmpegOptions));
}
[RelayCommand]
public async Task OpenFolderDialogAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path;
DownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathVIDEOS_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath,
defaultPath: CfgManager.PathVIDEOS_DIR
);
}
[RelayCommand]
public async Task OpenFolderDialogTempFolderAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path;
TempDownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathTEMP_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath,
defaultPath: CfgManager.PathTEMP_DIR
);
}
private async Task OpenFolderDialogAsyncInternal(Action<string> pathSetter, Func<string> pathGetter, string defaultPath){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
pathSetter(selectedFolder.Path.LocalPath);
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
pathSetter(finalPath);
CfgManager.WriteSettingsToFile();
}
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
partial void OnCurrentAppThemeChanged(ComboBoxItem? value){
if (value?.Content?.ToString() == "System"){
_faTheme.PreferSystemTheme = true;
} else if (value?.Content?.ToString() == "Dark"){
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
} else{
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
UpdateSettings();
}
partial void OnUseCustomAccentChanged(bool value){
if (value){
if (_faTheme.TryGetResource("SystemAccentColor", null, out var curColor)){
CustomAccentColor = (Color)curColor;
ListBoxColor = CustomAccentColor;
RaisePropertyChanged(nameof(CustomAccentColor));
RaisePropertyChanged(nameof(ListBoxColor));
}
} else{
CustomAccentColor = default;
ListBoxColor = default;
var color = Application.Current?.PlatformSettings?.GetColorValues().AccentColor1 ?? Colors.SlateBlue;
UpdateAppAccentColor(color);
}
}
partial void OnListBoxColorChanged(Color value){
if (value != null){
CustomAccentColor = value;
RaisePropertyChanged(nameof(CustomAccentColor));
UpdateAppAccentColor(value);
}
}
partial void OnCustomAccentColorChanged(Color value){
ListBoxColor = value;
RaisePropertyChanged(nameof(ListBoxColor));
UpdateAppAccentColor(value);
}
private void UpdateAppAccentColor(Color? color){
_faTheme.CustomAccentColor = color;
UpdateSettings();
}
private void Changes(object? sender, NotifyCollectionChangedEventArgs e){
UpdateSettings();
var dubs = SelectedDubLang.Select(item => item.Content?.ToString());
SelectedDubs = string.Join(", ", dubs) ?? "";
var subs = SelectedSubLang.Select(item => item.Content?.ToString());
SelectedSubs = string.Join(", ", subs) ?? "";
}
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)){
return;
}
UpdateSettings();
if (e.PropertyName is nameof(History)){
if (CrunchyrollManager.Instance.CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
if (!string.IsNullOrEmpty(decompressedJson)){
CrunchyrollManager.Instance.HistoryList = Helpers.Deserialize<ObservableCollection<HistorySeries>>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ??
new ObservableCollection<HistorySeries>();
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init();
}
}
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
_ = SonarrClient.Instance.RefreshSonarrLite();
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
}
[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){
CfgManager.EnableLogMode();
} else{
CfgManager.DisableLogMode();
}
}
} }

View file

@ -0,0 +1,394 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Reactive.Linq;
using System.Reactive.Threading.Tasks;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Controls;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using Newtonsoft.Json;
using ReactiveUI;
using JsonSerializer = Newtonsoft.Json.JsonSerializer;
namespace CRD.ViewModels;
public partial class UpcomingPageViewModel : ViewModelBase{
#region Query
private string query = @"query (
$season: MediaSeason,
$year: Int,
$format: MediaFormat,
$excludeFormat: MediaFormat,
$status: MediaStatus,
$minEpisodes: Int,
$page: Int,
){
Page(page: $page) {
pageInfo {
hasNextPage
total
}
media(
season: $season
seasonYear: $year
format: $format,
format_not: $excludeFormat,
status: $status,
episodes_greater: $minEpisodes,
isAdult: false,
type: ANIME,
sort: TITLE_ENGLISH,
) {
id
idMal
title {
romaji
native
english
}
startDate {
year
month
day
}
endDate {
year
month
day
}
status
season
format
genres
synonyms
duration
popularity
episodes
source(version: 2)
countryOfOrigin
hashtag
averageScore
siteUrl
description
bannerImage
isAdult
coverImage {
extraLarge
color
}
trailer {
id
site
thumbnail
}
externalLinks {
site
icon
color
url
}
rankings {
rank
type
season
allTime
}
studios(isMain: true) {
nodes {
id
name
siteUrl
}
}
relations {
edges {
relationType(version: 2)
node {
id
title {
romaji
native
english
}
siteUrl
}
}
}
airingSchedule(
notYetAired: true
perPage: 2
) {
nodes {
episode
airingAt
}
}
}
}
}";
#endregion
[ObservableProperty]
private AnilistSeries _selectedSeries;
[ObservableProperty]
private int _selectedIndex;
[ObservableProperty]
private bool _isLoading;
public ObservableCollection<SeasonViewModel> Seasons{ get; set; } =[];
public ObservableCollection<AnilistSeries> SelectedSeason{ get; set; } =[];
private SeasonViewModel currentSelection;
public UpcomingPageViewModel(){
LoadSeasons();
}
private async void LoadSeasons(){
Seasons = GetTargetSeasonsAndYears();
currentSelection = Seasons.Last();
currentSelection.IsSelected = true;
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
SelectedSeason.Clear();
foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries);
}
}
[RelayCommand]
public async Task SelectSeasonCommand(SeasonViewModel selectedSeason){
currentSelection.IsSelected = false;
currentSelection = selectedSeason;
currentSelection.IsSelected = true;
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
SelectedSeason.Clear();
foreach (var anilistSeries in list){
SelectedSeason.Add(anilistSeries);
}
}
[RelayCommand]
public void OpenTrailer(AnilistSeries series){
if (series.Trailer.Site.Equals("youtube")){
var url = "https://www.youtube.com/watch?v=" + series.Trailer.Id; // Replace with your video URL
Process.Start(new ProcessStartInfo{
FileName = url,
UseShellExecute = true
});
}
}
[RelayCommand]
public async void AddToHistory(AnilistSeries series){
if (!string.IsNullOrEmpty(series.CrunchyrollID)){
if (CrunchyrollManager.Instance.CrunOptions.History){
series.IsInHistory = true;
RaisePropertyChanged(nameof(series.IsInHistory));
var sucess = await CrunchyrollManager.Instance.History.CRUpdateSeries(series.CrunchyrollID, "");
series.IsInHistory = sucess;
RaisePropertyChanged(nameof(series.IsInHistory));
if (sucess){
MessageBus.Current.SendMessage(new ToastMessage($"Series added to History", ToastType.Information, 3));
} else{
MessageBus.Current.SendMessage(new ToastMessage($"Series couldn't get added to History\n(maybe not available in your region)", ToastType.Error, 3));
}
} else{
MessageBus.Current.SendMessage(new ToastMessage($"Series couldn't get added to History", ToastType.Error, 3));
}
} else{
MessageBus.Current.SendMessage(new ToastMessage($"Series couldn't get added to History", ToastType.Error, 3));
}
}
private async Task<List<AnilistSeries>> GetSeriesForSeason(string season, int year, bool forceRefresh){
if (ProgramManager.Instance.AnilistSeasons.ContainsKey(season + year) && !forceRefresh){
return ProgramManager.Instance.AnilistSeasons[season + year];
}
IsLoading = true;
var variables = new{
season,
year,
format = "TV",
page = 1
};
var payload = new{
query,
variables
};
string jsonPayload = JsonConvert.SerializeObject(payload, Formatting.Indented);
var request = new HttpRequestMessage(HttpMethod.Post, ApiUrls.Anilist);
request.Content = new StringContent(jsonPayload, Encoding.UTF8, "application/json");
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine($"Anilist Request Failed for {season} {year}");
return[];
}
AniListResponse aniListResponse = Helpers.Deserialize<AniListResponse>(response.ResponseContent, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ?? new AniListResponse();
var list = aniListResponse.Data?.Page?.Media ??[];
list = list.Where(ele => ele.ExternalLinks != null && ele.ExternalLinks.Any(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase))).ToList();
foreach (var anilistEle in list){
anilistEle.ThumbnailImage = await Helpers.LoadImage(anilistEle.CoverImage.ExtraLarge, 185, 265);
anilistEle.Description = anilistEle.Description
.Replace("<i>", "")
.Replace("</i>", "")
.Replace("<BR>", "")
.Replace("<br>", "");
if (anilistEle.ExternalLinks != null){
var url = anilistEle.ExternalLinks.First(external =>
string.Equals(external.Site, "Crunchyroll", StringComparison.OrdinalIgnoreCase)).Url;
string pattern = @"series\/([^\/]+)";
Match match = Regex.Match(url, pattern);
if (match.Success){
anilistEle.CrunchyrollID = match.Groups[1].Value;
anilistEle.HasCrID = true;
if (CrunchyrollManager.Instance.CrunOptions.History){
var historyIDs = new HashSet<string>(CrunchyrollManager.Instance.HistoryList.Select(item => item.SeriesId ?? ""));
if (historyIDs.Contains(anilistEle.CrunchyrollID)){
anilistEle.IsInHistory = true;
}
}
} else{
Uri uri = new Uri(url);
if (uri.Host == "www.crunchyroll.com"
&& uri.AbsolutePath != "/"
&& (uri.Scheme == Uri.UriSchemeHttp || uri.Scheme == Uri.UriSchemeHttps)){
HttpRequestMessage getUrlRequest = new HttpRequestMessage(HttpMethod.Head, url);
string? finalUrl = "";
try{
HttpResponseMessage getUrlResponse = await HttpClientReq.Instance.GetHttpClient().SendAsync(getUrlRequest);
finalUrl = getUrlResponse.RequestMessage?.RequestUri?.ToString();
} catch (Exception ex){
Console.WriteLine($"Error: {ex.Message}");
}
Match match2 = Regex.Match(finalUrl ?? string.Empty, pattern);
if (match2.Success){
anilistEle.CrunchyrollID = match2.Groups[1].Value;
anilistEle.HasCrID = true;
if (CrunchyrollManager.Instance.CrunOptions.History){
var historyIDs = new HashSet<string>(CrunchyrollManager.Instance.HistoryList.Select(item => item.SeriesId ?? ""));
if (historyIDs.Contains(anilistEle.CrunchyrollID)){
anilistEle.IsInHistory = true;
}
}
} else{
anilistEle.CrunchyrollID = "";
anilistEle.HasCrID = false;
}
} else{
anilistEle.CrunchyrollID = "";
anilistEle.HasCrID = false;
}
}
}
}
ProgramManager.Instance.AnilistSeasons[season + year] = list;
IsLoading = false;
return list;
}
private ObservableCollection<SeasonViewModel> GetTargetSeasonsAndYears(){
DateTime now = DateTime.Now;
int currentMonth = now.Month;
int currentYear = now.Year;
string currentSeason;
if (currentMonth >= 1 && currentMonth <= 3)
currentSeason = "WINTER";
else if (currentMonth >= 4 && currentMonth <= 6)
currentSeason = "SPRING";
else if (currentMonth >= 7 && currentMonth <= 9)
currentSeason = "SUMMER";
else
currentSeason = "FALL";
var seasons = new List<string>{ "WINTER", "SPRING", "SUMMER", "FALL" };
int currentSeasonIndex = seasons.IndexOf(currentSeason);
var targetSeasons = new ObservableCollection<SeasonViewModel>();
// Includes: -2 (two seasons ago), -1 (previous), 0 (current), 1 (next)
for (int i = -2; i <= 1; i++){
int targetIndex = (currentSeasonIndex + i + 4) % 4;
string targetSeason = seasons[targetIndex];
int targetYear = currentYear;
if (i < 0 && targetIndex == 3){
targetYear--;
} else if (i > 0 && targetIndex == 0){
targetYear++;
}
targetSeasons.Add(new SeasonViewModel(){ Season = targetSeason, Year = targetYear });
}
return targetSeasons;
}
public void SelectionChangedOfSeries(AnilistSeries? value){
SelectedSeries = null;
SelectedIndex = -1;
}
partial void OnSelectedSeriesChanged(AnilistSeries? value){
SelectionChangedOfSeries(value);
}
}

View file

@ -0,0 +1,42 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Utils.Structs;
using FluentAvalonia.UI.Controls;
namespace CRD.ViewModels.Utils;
public partial class ContentDialogDropdownSelectViewModel : ViewModelBase{
private readonly ContentDialog dialog;
public ObservableCollection<StringItem> DropDownItemList{ get; } = new(){ };
[ObservableProperty]
private StringItem _selectedDropdownItem = new StringItem();
[ObservableProperty]
private string _episodeInfo;
public ContentDialogDropdownSelectViewModel(ContentDialog dialog, string episodeInfo, List<string> dropdownItems){
if (dialog is null){
throw new ArgumentNullException(nameof(dialog));
}
this.dialog = dialog;
dialog.Closed += DialogOnClosed;
dialog.PrimaryButtonClick += SaveButton;
EpisodeInfo = episodeInfo;
foreach (var dropdownItem in dropdownItems){
DropDownItemList.Add(new StringItem(){ stringValue = dropdownItem });
}
}
private async void SaveButton(ContentDialog sender, ContentDialogButtonClickEventArgs args){
dialog.PrimaryButtonClick -= SaveButton;
}
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
dialog.Closed -= DialogOnClosed;
}
}

View file

@ -1,5 +1,4 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;

View file

@ -0,0 +1,516 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
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.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using FluentAvalonia.Styling;
namespace CRD.ViewModels.Utils;
// ReSharper disable InconsistentNaming
public partial class GeneralSettingsViewModel : ViewModelBase{
[ObservableProperty]
private string _currentVersion;
[ObservableProperty]
private bool _downloadToTempFolder;
[ObservableProperty]
private bool _history;
[ObservableProperty]
private bool _historyAddSpecials;
[ObservableProperty]
private bool _historyCountSonarr;
[ObservableProperty]
private double? _simultaneousDownloads;
[ObservableProperty]
private double? _downloadSpeed;
[ObservableProperty]
private ComboBoxItem _selectedHistoryLang;
[ObservableProperty]
private ComboBoxItem? _currentAppTheme;
[ObservableProperty]
private bool _useCustomAccent;
[ObservableProperty]
private string _backgroundImagePath;
[ObservableProperty]
private double? _backgroundImageOpacity;
[ObservableProperty]
private double? _backgroundImageBlurRadius;
[ObservableProperty]
private Color _listBoxColor;
[ObservableProperty]
private Color _customAccentColor = Colors.SlateBlue;
[ObservableProperty]
private string _sonarrHost = "localhost";
[ObservableProperty]
private string _sonarrPort = "8989";
[ObservableProperty]
private string _sonarrApiKey = "";
[ObservableProperty]
private bool _sonarrUseSsl = false;
[ObservableProperty]
private bool _sonarrUseSonarrNumbering = false;
[ObservableProperty]
private bool _logMode = false;
public ObservableCollection<Color> PredefinedColors{ get; } = new(){
Color.FromRgb(255, 185, 0),
Color.FromRgb(255, 140, 0),
Color.FromRgb(247, 99, 12),
Color.FromRgb(202, 80, 16),
Color.FromRgb(218, 59, 1),
Color.FromRgb(239, 105, 80),
Color.FromRgb(209, 52, 56),
Color.FromRgb(255, 67, 67),
Color.FromRgb(231, 72, 86),
Color.FromRgb(232, 17, 35),
Color.FromRgb(234, 0, 94),
Color.FromRgb(195, 0, 82),
Color.FromRgb(227, 0, 140),
Color.FromRgb(191, 0, 119),
Color.FromRgb(194, 57, 179),
Color.FromRgb(154, 0, 137),
Color.FromRgb(0, 120, 212),
Color.FromRgb(0, 99, 177),
Color.FromRgb(142, 140, 216),
Color.FromRgb(107, 105, 214),
Colors.SlateBlue,
Color.FromRgb(135, 100, 184),
Color.FromRgb(116, 77, 169),
Color.FromRgb(177, 70, 194),
Color.FromRgb(136, 23, 152),
Color.FromRgb(0, 153, 188),
Color.FromRgb(45, 125, 154),
Color.FromRgb(0, 183, 195),
Color.FromRgb(3, 131, 135),
Color.FromRgb(0, 178, 148),
Color.FromRgb(1, 133, 116),
Color.FromRgb(0, 204, 106),
Color.FromRgb(16, 137, 62),
Color.FromRgb(122, 117, 116),
Color.FromRgb(93, 90, 88),
Color.FromRgb(104, 118, 138),
Color.FromRgb(81, 92, 107),
Color.FromRgb(86, 124, 115),
Color.FromRgb(72, 104, 96),
Color.FromRgb(73, 130, 5),
Color.FromRgb(16, 124, 16),
Color.FromRgb(118, 118, 118),
Color.FromRgb(76, 74, 72),
Color.FromRgb(105, 121, 126),
Color.FromRgb(74, 84, 89),
Color.FromRgb(100, 124, 100),
Color.FromRgb(82, 94, 84),
Color.FromRgb(132, 117, 69),
Color.FromRgb(126, 115, 95)
};
public ObservableCollection<ComboBoxItem> AppThemes{ get; } = new(){
new ComboBoxItem(){ Content = "System" },
new ComboBoxItem(){ Content = "Light" },
new ComboBoxItem(){ Content = "Dark" },
};
public ObservableCollection<ComboBoxItem> HistoryLangList{ get; } = new(){
new ComboBoxItem(){ Content = "default" },
new ComboBoxItem(){ Content = "de-DE" },
new ComboBoxItem(){ Content = "en-US" },
new ComboBoxItem(){ Content = "es-419" },
new ComboBoxItem(){ Content = "es-ES" },
new ComboBoxItem(){ Content = "fr-FR" },
new ComboBoxItem(){ Content = "it-IT" },
new ComboBoxItem(){ Content = "pt-BR" },
new ComboBoxItem(){ Content = "pt-PT" },
new ComboBoxItem(){ Content = "ru-RU" },
new ComboBoxItem(){ Content = "hi-IN" },
new ComboBoxItem(){ Content = "ar-SA" },
};
[ObservableProperty]
private string _downloadDirPath;
[ObservableProperty]
private bool _proxyEnabled;
[ObservableProperty]
private string _proxyHost;
[ObservableProperty]
private double? _proxyPort;
[ObservableProperty]
private string _tempDownloadDirPath;
[ObservableProperty]
private string _currentIp = "";
private readonly FluentAvaloniaTheme _faTheme;
private bool settingsLoaded;
private IStorageProvider _storageProvider;
public GeneralSettingsViewModel(){
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
var version = Assembly.GetExecutingAssembly().GetName().Version;
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
_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;
}
CrDownloadOptions options = CrunchyrollManager.Instance.CrunOptions;
BackgroundImageBlurRadius = options.BackgroundImageBlurRadius;
BackgroundImageOpacity = options.BackgroundImageOpacity;
BackgroundImagePath = options.BackgroundImagePath ?? string.Empty;
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath;
ComboBoxItem? historyLang = HistoryLangList.FirstOrDefault(a => a.Content != null && (string)a.Content == options.HistoryLang) ?? null;
SelectedHistoryLang = historyLang ?? HistoryLangList[0];
var props = options.SonarrProperties;
if (props != null){
SonarrUseSsl = props.UseSsl;
SonarrUseSonarrNumbering = props.UseSonarrNumbering;
SonarrHost = props.Host + "";
SonarrPort = props.Port + "";
SonarrApiKey = props.ApiKey + "";
}
ProxyEnabled = options.ProxyEnabled;
ProxyHost = options.ProxyHost ?? "";
ProxyPort = options.ProxyPort;
HistoryAddSpecials = options.HistoryAddSpecials;
HistoryCountSonarr = options.HistoryCountSonarr;
DownloadSpeed = options.DownloadSpeedLimit;
DownloadToTempFolder = options.DownloadToTempFolder;
SimultaneousDownloads = options.SimultaneousDownloads;
LogMode = options.LogMode;
ComboBoxItem? theme = AppThemes.FirstOrDefault(a => a.Content != null && (string)a.Content == options.Theme) ?? null;
CurrentAppTheme = theme ?? AppThemes[0];
if (!string.IsNullOrEmpty(options.AccentColor) && options.AccentColor != Application.Current?.PlatformSettings?.GetColorValues().AccentColor1.ToString()){
UseCustomAccent = true;
}
History = options.History;
settingsLoaded = true;
}
private void UpdateSettings(){
if (!settingsLoaded){
return;
}
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
CrunchyrollManager.Instance.CrunOptions.DownloadToTempFolder = DownloadToTempFolder;
CrunchyrollManager.Instance.CrunOptions.HistoryAddSpecials = HistoryAddSpecials;
CrunchyrollManager.Instance.CrunOptions.HistoryCountSonarr = HistoryCountSonarr;
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);
string historyLang = SelectedHistoryLang.Content + "";
CrunchyrollManager.Instance.CrunOptions.HistoryLang = historyLang != "default" ? historyLang : CrunchyrollManager.Instance.DefaultLocale;
CrunchyrollManager.Instance.CrunOptions.Theme = CurrentAppTheme?.Content + "";
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;
var props = new SonarrProperties();
props.UseSsl = SonarrUseSsl;
props.UseSonarrNumbering = SonarrUseSonarrNumbering;
props.Host = SonarrHost;
if (int.TryParse(SonarrPort, out var portNumber)){
props.Port = portNumber;
} else{
props.Port = 8989;
}
props.ApiKey = SonarrApiKey;
CrunchyrollManager.Instance.CrunOptions.SonarrProperties = props;
CrunchyrollManager.Instance.CrunOptions.LogMode = LogMode;
CfgManager.WriteSettingsToFile();
}
[RelayCommand]
public void ClearDownloadDirPath(){
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = string.Empty;
DownloadDirPath = CfgManager.PathVIDEOS_DIR;
}
[RelayCommand]
public void ClearDownloadTempDirPath(){
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = string.Empty;
TempDownloadDirPath = CfgManager.PathTEMP_DIR;
}
[RelayCommand]
public async Task OpenFolderDialogAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadDirPath = path;
DownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathVIDEOS_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadDirPath,
defaultPath: CfgManager.PathVIDEOS_DIR
);
}
[RelayCommand]
public async Task OpenFolderDialogTempFolderAsync(){
await OpenFolderDialogAsyncInternal(
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath = path;
TempDownloadDirPath = string.IsNullOrEmpty(path) ? CfgManager.PathTEMP_DIR : path;
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadTempDirPath,
defaultPath: CfgManager.PathTEMP_DIR
);
}
private async Task OpenFolderDialogAsyncInternal(Action<string> pathSetter, Func<string> pathGetter, string defaultPath){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
var result = await _storageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions{
Title = "Select Folder"
});
if (result.Count > 0){
var selectedFolder = result[0];
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
pathSetter(selectedFolder.Path.LocalPath);
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
pathSetter(finalPath);
CfgManager.WriteSettingsToFile();
}
}
[RelayCommand]
public void ClearBackgroundImagePath(){
CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = string.Empty;
BackgroundImagePath = string.Empty;
Helpers.SetBackgroundImage(string.Empty);
}
[RelayCommand]
public async Task OpenImageFileDialogAsyncInternalBackgroundImage(){
await OpenImageFileDialogAsyncInternal(
pathSetter: (path) => {
CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = path;
BackgroundImagePath = path;
Helpers.SetBackgroundImage(path, BackgroundImageOpacity, BackgroundImageBlurRadius);
},
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath,
defaultPath: string.Empty
);
}
private async Task OpenImageFileDialogAsyncInternal(Action<string> pathSetter, Func<string> pathGetter, string defaultPath){
if (_storageProvider == null){
Console.Error.WriteLine("StorageProvider must be set before using the dialog.");
throw new InvalidOperationException("StorageProvider must be set before using the dialog.");
}
// Open the file picker dialog with only image file types allowed
var result = await _storageProvider.OpenFilePickerAsync(new FilePickerOpenOptions{
Title = "Select Image File",
FileTypeFilter = new List<FilePickerFileType>{
new FilePickerFileType("Image Files"){
Patterns = new[]{ "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif" }
}
},
AllowMultiple = false
});
if (result.Count > 0){
var selectedFile = result[0];
Console.WriteLine($"Selected file: {selectedFile.Path.LocalPath}");
pathSetter(selectedFile.Path.LocalPath);
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
pathSetter(finalPath);
CfgManager.WriteSettingsToFile();
}
}
partial void OnCurrentAppThemeChanged(ComboBoxItem? value){
if (value?.Content?.ToString() == "System"){
_faTheme.PreferSystemTheme = true;
} else if (value?.Content?.ToString() == "Dark"){
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Dark;
} else{
_faTheme.PreferSystemTheme = false;
Application.Current.RequestedThemeVariant = ThemeVariant.Light;
}
UpdateSettings();
}
partial void OnUseCustomAccentChanged(bool value){
if (value){
if (_faTheme.TryGetResource("SystemAccentColor", null, out var curColor)){
CustomAccentColor = (Color)curColor;
ListBoxColor = CustomAccentColor;
RaisePropertyChanged(nameof(CustomAccentColor));
RaisePropertyChanged(nameof(ListBoxColor));
}
} else{
CustomAccentColor = default;
ListBoxColor = default;
var color = Application.Current?.PlatformSettings?.GetColorValues().AccentColor1 ?? Colors.SlateBlue;
UpdateAppAccentColor(color);
}
}
partial void OnListBoxColorChanged(Color value){
if (value != null){
CustomAccentColor = value;
RaisePropertyChanged(nameof(CustomAccentColor));
UpdateAppAccentColor(value);
}
}
partial void OnCustomAccentColorChanged(Color value){
ListBoxColor = value;
RaisePropertyChanged(nameof(ListBoxColor));
UpdateAppAccentColor(value);
}
private void UpdateAppAccentColor(Color? color){
_faTheme.CustomAccentColor = color;
UpdateSettings();
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e){
base.OnPropertyChanged(e);
if (e.PropertyName is
nameof(CustomAccentColor)
or nameof(ListBoxColor)
or nameof(CurrentAppTheme)
or nameof(UseCustomAccent)
or nameof(LogMode)){
return;
}
UpdateSettings();
if (e.PropertyName is nameof(History)){
if (CrunchyrollManager.Instance.CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
var decompressedJson = CfgManager.DecompressJsonFile(CfgManager.PathCrHistory);
if (!string.IsNullOrEmpty(decompressedJson)){
CrunchyrollManager.Instance.HistoryList = Helpers.Deserialize<ObservableCollection<HistorySeries>>(decompressedJson, CrunchyrollManager.Instance.SettingsJsonSerializerSettings) ??
new ObservableCollection<HistorySeries>();
foreach (var historySeries in CrunchyrollManager.Instance.HistoryList){
historySeries.Init();
foreach (var historySeriesSeason in historySeries.Seasons){
historySeriesSeason.Init();
}
}
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
_ = SonarrClient.Instance.RefreshSonarrLite();
} else{
CrunchyrollManager.Instance.HistoryList =[];
}
}
if (!string.IsNullOrEmpty(BackgroundImagePath) && e.PropertyName is nameof(BackgroundImageBlurRadius) or nameof(BackgroundImageOpacity)){
Helpers.SetBackgroundImage(BackgroundImagePath, BackgroundImageOpacity, BackgroundImageBlurRadius);
}
}
[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){
CfgManager.EnableLogMode();
} else{
CfgManager.DisableLogMode();
}
}
}

View file

@ -1,5 +1,4 @@
using System; using System.ComponentModel;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace CRD.ViewModels; namespace CRD.ViewModels;

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views; namespace CRD.Views;

View file

@ -75,6 +75,10 @@
Width="120" Width="120"
Height="180" /> Height="180" />
</Grid> </Grid>
@ -91,6 +95,14 @@
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap" <TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
Text="{Binding Description}"> Text="{Binding Description}">
</TextBlock> </TextBlock>
<StackPanel Grid.Row="3" VerticalAlignment="Bottom" HorizontalAlignment="Left"
IsVisible="{Binding IsInHistory}" Margin="0 10 10 10" >
<controls:SymbolIcon Symbol="Library" FontSize="32" />
<ToolTip.Tip>
<TextBlock Text="Series is in History" FontSize="15" />
</ToolTip.Tip>
</StackPanel>
</Grid> </Grid>

View file

@ -27,7 +27,7 @@
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Margin="10 10 0 0" HorizontalAlignment="Center" <Button Grid.Row="0" Grid.Column="0" Margin="10 10 0 0" HorizontalAlignment="Center"
IsEnabled="{Binding !CustomCalendar}" IsEnabled="{Binding PrevButtonEnabled}"
Command="{Binding PrevWeek}"> Command="{Binding PrevWeek}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ChevronLeft" FontSize="18" /> <controls:SymbolIcon Symbol="ChevronLeft" FontSize="18" />
@ -65,19 +65,19 @@
<StackPanel> <StackPanel>
<controls:SettingsExpander IsVisible="{Binding !CustomCalendar}" Header="Simulcast Calendar Language"> <controls:SettingsExpander IsVisible="{Binding !CustomCalendar}" Header="Simulcast Calendar Language">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<ComboBox HorizontalAlignment="Center" Margin="10 0 0 0" MinWidth="200" <ComboBox HorizontalAlignment="Center" Margin="10 0 0 0" MinWidth="200"
SelectedItem="{Binding CurrentCalendarLanguage}" SelectedItem="{Binding CurrentCalendarLanguage}"
ItemsSource="{Binding CalendarLanguage}"> ItemsSource="{Binding CalendarLanguage}">
</ComboBox> </ComboBox>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander Header="Custom Calendar"> <controls:SettingsExpander Header="Custom Calendar">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding CustomCalendar}" <CheckBox IsChecked="{Binding CustomCalendar}"
@ -85,10 +85,10 @@
</CheckBox> </CheckBox>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar Dub Filter"> <controls:SettingsExpander IsVisible="{Binding CustomCalendar}" Header="Custom Calendar Dub Filter">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<ComboBox HorizontalAlignment="Center" Margin="5 0 0 5" MinWidth="200" <ComboBox HorizontalAlignment="Center" Margin="5 0 0 5" MinWidth="200"
@ -99,12 +99,12 @@
Content="Filter by episode air date" Margin="5 5 0 0"> Content="Filter by episode air date" Margin="5 5 0 0">
</CheckBox> </CheckBox>
</StackPanel> </StackPanel>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
</controls:SettingsExpander> </controls:SettingsExpander>
<controls:SettingsExpander Header="Calendar "> <controls:SettingsExpander Header="Calendar ">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding HideDubs}" <CheckBox IsChecked="{Binding HideDubs}"
@ -112,9 +112,9 @@
</CheckBox> </CheckBox>
</controls:SettingsExpander.Footer> </controls:SettingsExpander.Footer>
</controls:SettingsExpander> </controls:SettingsExpander>
</StackPanel> </StackPanel>
</Border> </Border>
</Popup> </Popup>
@ -125,7 +125,7 @@
<Button Grid.Row="0" Grid.Column="2" Margin="0 0 10 0" HorizontalAlignment="Center" <Button Grid.Row="0" Grid.Column="2" Margin="0 0 10 0" HorizontalAlignment="Center"
IsEnabled="{Binding !CustomCalendar}" IsEnabled="{Binding NextButtonEnabled}"
Command="{Binding NextWeek}"> Command="{Binding NextWeek}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ChevronRight" FontSize="18" /> <controls:SymbolIcon Symbol="ChevronRight" FontSize="18" />
@ -178,17 +178,27 @@
Margin="0,0,0,0" /> Margin="0,0,0,0" />
<Grid HorizontalAlignment="Center"> <Grid HorizontalAlignment="Center">
<Grid> <Grid>
<Image HorizontalAlignment="Center" Source="../Assets/coming_soon_ep.jpg" /> <Image HorizontalAlignment="Center" IsVisible="{Binding !AnilistEpisode}" Source="../Assets/coming_soon_ep.jpg" />
<Image HorizontalAlignment="Center" Source="{Binding ImageBitmap}" /> <Image HorizontalAlignment="Center" MaxHeight="150" Source="{Binding ImageBitmap}" />
</Grid> </Grid>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Left"> <StackPanel VerticalAlignment="Top" HorizontalAlignment="Left">
<TextBlock VerticalAlignment="Center" TextAlignment="Center"
Margin="0 0 5 0" Width="30" Height="30" <Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 10 0"
Background="Black" Opacity="0.8" Background="#f78c25" Opacity="1"
Text="{Binding EpisodeNumber}" Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} ">
Padding="0,5,0,0" /> <TextBlock VerticalAlignment="Center" TextAlignment="Center"
Width="30" Height="30"
Text="{Binding EpisodeNumber}"
Padding="0,5,0,0" />
</Border>
<!-- <TextBlock VerticalAlignment="Center" TextAlignment="Center" -->
<!-- Margin="0 0 5 0" Width="30" Height="30" -->
<!-- Background="Black" Opacity="0.8" -->
<!-- Text="{Binding EpisodeNumber}" -->
<!-- Padding="0,5,0,0" /> -->
</StackPanel> </StackPanel>
<StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right" <StackPanel VerticalAlignment="Bottom" HorizontalAlignment="Right"
IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5"> IsVisible="{Binding IsPremiumOnly}" Margin="0,0,5,5">
@ -217,8 +227,9 @@
</TextBlock> </TextBlock>
<Button HorizontalAlignment="Center" Content="Download" <Button HorizontalAlignment="Center" Content="Download"
IsEnabled="{Binding HasPassed}" IsEnabled="{Binding HasPassed}"
IsVisible="{Binding HasPassed}"
Command="{Binding AddEpisodeToQue}" Command="{Binding AddEpisodeToQue}"
CommandParameter="{Binding EpisodeUrl}" /> />
</StackPanel> </StackPanel>
</Border> </Border>
</DataTemplate> </DataTemplate>

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views; namespace CRD.Views;

View file

@ -7,7 +7,6 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
x:DataType="vm:DownloadsPageViewModel" x:DataType="vm:DownloadsPageViewModel"
x:Class="CRD.Views.DownloadsPageView" x:Class="CRD.Views.DownloadsPageView"
xmlns:local="clr-namespace:CRD.Utils"
xmlns:ui="clr-namespace:CRD.Utils.UI"> xmlns:ui="clr-namespace:CRD.Utils.UI">
<UserControl.Resources> <UserControl.Resources>
@ -17,12 +16,11 @@
<Grid> <Grid>
<Grid.RowDefinitions> <Grid.RowDefinitions>
<RowDefinition Height="Auto" /> <!-- For the TextBox --> <RowDefinition Height="Auto" />
<RowDefinition Height="*" /> <!-- For the ListBox to take remaining space --> <RowDefinition Height="*" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right"> <StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right">
<!-- <Button Click="Button_OnClick">Test Download</Button> -->
<ToggleSwitch HorizontalAlignment="Right" Margin="0 0 10 0 " IsChecked="{Binding RemoveFinished}" OffContent="Remove Finished" OnContent="Remove Finished"></ToggleSwitch> <ToggleSwitch HorizontalAlignment="Right" Margin="0 0 10 0 " IsChecked="{Binding RemoveFinished}" OffContent="Remove Finished" OnContent="Remove Finished"></ToggleSwitch>
<ToggleSwitch HorizontalAlignment="Right" Margin="0 0 10 0 " IsChecked="{Binding AutoDownload}" OffContent="Auto Download" OnContent="Auto Download"></ToggleSwitch> <ToggleSwitch HorizontalAlignment="Right" Margin="0 0 10 0 " IsChecked="{Binding AutoDownload}" OffContent="Auto Download" OnContent="Auto Download"></ToggleSwitch>
</StackPanel> </StackPanel>
@ -38,9 +36,6 @@
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Image -->
<!-- <Image Grid.Column="0" Width="208" Height="117" Source="{Binding ImageBitmap}" -->
<!-- Stretch="Fill" /> -->
<Grid> <Grid>
<Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" /> <Image HorizontalAlignment="Center" Width="208" Height="117" Source="../Assets/coming_soon_ep.jpg" />
@ -97,7 +92,7 @@
<Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" > <Grid Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="3" VerticalAlignment="Bottom" >
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="*" /> <!-- Takes up most space for the title --> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>

View file

@ -1,8 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Interactivity;
using CRD.Downloader;
using CRD.ViewModels;
namespace CRD.Views; namespace CRD.Views;

View file

@ -38,9 +38,9 @@
VerticalAlignment="Center" VerticalAlignment="Center"
Command="{Binding RefreshAll}" Command="{Binding RefreshAll}"
IsEnabled="{Binding !ProgramManager.FetchingData}"> IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<controls:SymbolIcon Symbol="Sync" FontSize="32" /> <controls:SymbolIcon Symbol="Sync" FontSize="32" />
<TextBlock Text="Refresh Filtered" TextWrapping="Wrap" FontSize="12"></TextBlock> <TextBlock Text="Refresh Filtered" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</Button> </Button>
@ -50,7 +50,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}"> IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Import" FontSize="32" /> <controls:SymbolIcon Symbol="Import" FontSize="32" />
<TextBlock Text="Add To Queue" TextWrapping="Wrap" FontSize="12"></TextBlock> <TextBlock Text="Add To Queue" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</Button> </Button>
@ -60,7 +60,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}"> IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Edit" FontSize="32" /> <controls:SymbolIcon Symbol="Edit" FontSize="32" />
<TextBlock Text="Edit" TextWrapping="Wrap" FontSize="12"></TextBlock> <TextBlock Text="Edit" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
@ -74,7 +74,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}"> IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:ImageIcon VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 1 0 0" Source="../Assets/sonarr.png" Width="30" Height="30" /> <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> <TextBlock Text="Sonarr" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
@ -123,7 +123,7 @@
IsChecked="{Binding ViewSelectionOpen}"> IsChecked="{Binding ViewSelectionOpen}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="View" FontSize="32" /> <controls:SymbolIcon Symbol="View" FontSize="32" />
<TextBlock Text="View" FontSize="12"></TextBlock> <TextBlock Text="View" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
@ -147,7 +147,7 @@
IsChecked="{Binding SortingSelectionOpen}"> IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="32" /> <controls:SymbolIcon Symbol="Sort" FontSize="32" />
<TextBlock Text="Sort" FontSize="12"></TextBlock> <TextBlock Text="Sort" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
@ -178,7 +178,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}"> IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical"> <StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" /> <controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock> <TextBlock Text="Filter" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
<Popup IsLightDismissEnabled="True" <Popup IsLightDismissEnabled="True"
@ -214,80 +214,82 @@
</ListBox.ItemsPanel> </ListBox.ItemsPanel>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<Border>
<Grid>
<StackPanel Orientation="Vertical" HorizontalAlignment="Center"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterWidth}"
Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterHeight}"
Margin="5">
<Grid>
<Image Source="{Binding ThumbnailImage}"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageHeight}">
</Image>
<Grid> <Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 0 10"
<StackPanel Orientation="Vertical" HorizontalAlignment="Center" Background="#f78c25" Opacity="1"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterWidth}" Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} "
Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterHeight}" IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}">
Margin="5"> <TextBlock VerticalAlignment="Center" TextAlignment="Center" Width="30"
<Grid> Height="30"
<Image Source="{Binding ThumbnailImage}" Text="{Binding NewEpisodes}"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}" Padding="0,5,0,0" />
Height="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageHeight}"> </Border>
</Image>
<Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 0 10" </Grid>
Background="#f78c25" Opacity="1"
Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} "
IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}">
<TextBlock VerticalAlignment="Center" TextAlignment="Center" Width="30"
Height="30"
Text="{Binding NewEpisodes}"
Padding="0,5,0,0" />
</Border>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding SeriesTitle}"
TextWrapping="Wrap"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
Height="35"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SeriesTitle}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding SonarrNextAirDate}"
TextWrapping="Wrap"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
MaxHeight="20"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SonarrNextAirDate}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).EditMode}">
<Button
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
FontStyle="Italic"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).ProgramManager.FetchingData}">
<ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Delete" FontSize="32" />
</StackPanel>
</Button>
</Grid> </Grid>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center" <Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding FetchingData}">
Text="{Binding SeriesTitle}" <controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
TextWrapping="Wrap" </Grid>
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
Height="35"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SeriesTitle}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding SonarrNextAirDate}"
TextWrapping="Wrap"
Width="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterImageWidth}"
FontSize="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).PosterTextSize}"
MaxHeight="20"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding SonarrNextAirDate}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).EditMode}">
<Button
Background="Transparent"
BorderThickness="0"
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
FontStyle="Italic"
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).RemoveSeries}"
CommandParameter="{Binding SeriesId}"
IsEnabled="{Binding !$parent[UserControl].((vm:HistoryPageViewModel)DataContext).ProgramManager.FetchingData}">
<ToolTip.Tip>
<TextBlock Text="Remove Series" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Delete" FontSize="32" />
</StackPanel>
</Button>
</Grid> </Grid>
</Border>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding FetchingData}">
<controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
</Grid>
</Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
@ -422,7 +424,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding EditModeEnabled}"> IsVisible="{Binding EditModeEnabled}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" /> <controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
@ -435,11 +437,51 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel> <StackPanel>
<controls:SettingsExpander Header="Language Override"> <controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
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=OverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -476,7 +518,7 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
@ -724,7 +766,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}"> IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" /> <controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
@ -737,11 +779,52 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel> <StackPanel>
<controls:SettingsExpander Header="Language Override"> <controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
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=SeasonOverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -778,7 +861,7 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">

View file

@ -10,10 +10,11 @@
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Icon="/Assets/app_icon.ico" Icon="/Assets/app_icon.ico"
Title="Crunchy-Downloader"> Title="Crunchy-Downloader">
<Design.DataContext> <Design.DataContext>
<vm:MainWindowViewModel /> <vm:MainWindowViewModel />
</Design.DataContext> </Design.DataContext>
<Grid Name="MainGrid"> <Grid Name="MainGrid">
<ContentControl x:Name="MainContent"> <ContentControl x:Name="MainContent">
<Grid RowDefinitions="Auto, *"> <Grid RowDefinitions="Auto, *">
@ -60,6 +61,8 @@
IconSource="Add" /> IconSource="Add" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Calendar" Tag="Calendar" <ui:NavigationViewItem Classes="SampleAppNav" Content="Calendar" Tag="Calendar"
IconSource="Calendar" /> IconSource="Calendar" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Seasons" Tag="Seasons"
IconSource="Clock" />
<ui:NavigationViewItem IsEnabled="{Binding ProgramManager.FinishedLoading}" Classes="SampleAppNav" Content="History" Tag="History" <ui:NavigationViewItem IsEnabled="{Binding ProgramManager.FinishedLoading}" Classes="SampleAppNav" Content="History" Tag="History"
IconSource="Library" /> IconSource="Library" />
</ui:NavigationView.MenuItems> </ui:NavigationView.MenuItems>

View file

@ -5,7 +5,7 @@ using System.Linq;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Platform; using CRD.Downloader;
using CRD.Downloader.Crunchyroll; using CRD.Downloader.Crunchyroll;
using CRD.Utils; using CRD.Utils;
using CRD.Utils.Files; using CRD.Utils.Files;
@ -13,7 +13,6 @@ using CRD.Utils.Structs;
using CRD.Utils.Updater; using CRD.Utils.Updater;
using CRD.ViewModels; using CRD.ViewModels;
using CRD.Views.Utils; using CRD.Views.Utils;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
using Newtonsoft.Json; using Newtonsoft.Json;
@ -56,14 +55,15 @@ public partial class MainWindow : AppWindow{
private Size _restoreSize; private Size _restoreSize;
public MainWindow(){ public MainWindow(){
ProgramManager.Instance.StorageProvider = StorageProvider;
AvaloniaXamlLoader.Load(this); AvaloniaXamlLoader.Load(this);
InitializeComponent(); InitializeComponent();
ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment; ExtendClientAreaTitleBarHeightHint = TitleBarHeightAdjustment;
TitleBar.Height = TitleBarHeightAdjustment; TitleBar.Height = TitleBarHeightAdjustment;
TitleBar.ExtendsContentIntoTitleBar = true; TitleBar.ExtendsContentIntoTitleBar = true;
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
Opened += OnOpened; Opened += OnOpened;
Closing += OnClosing; Closing += OnClosing;
@ -83,17 +83,13 @@ public partial class MainWindow : AppWindow{
if (message.Refresh){ if (message.Refresh){
navigationStack.Pop(); navigationStack.Pop();
var viewModel = Activator.CreateInstance(message.ViewModelType); var viewModel = Activator.CreateInstance(message.ViewModelType);
if (viewModel is SeriesPageViewModel){
((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider);
}
navigationStack.Push(viewModel); navigationStack.Push(viewModel);
nv.Content = viewModel; nv.Content = viewModel;
} else if (!message.Back && message.ViewModelType != null){ } else if (!message.Back && message.ViewModelType != null){
var viewModel = Activator.CreateInstance(message.ViewModelType); var viewModel = Activator.CreateInstance(message.ViewModelType);
if (viewModel is SeriesPageViewModel){
((SeriesPageViewModel)viewModel).SetStorageProvider(StorageProvider);
}
navigationStack.Push(viewModel); navigationStack.Push(viewModel);
nv.Content = viewModel; nv.Content = viewModel;
@ -143,21 +139,20 @@ public partial class MainWindow : AppWindow{
break; break;
case "History": case "History":
navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel)); navView.Content = Activator.CreateInstance(typeof(HistoryPageViewModel));
if (navView.Content is HistoryPageViewModel){
((HistoryPageViewModel)navView.Content).SetStorageProvider(StorageProvider);
}
navigationStack.Clear(); navigationStack.Clear();
navigationStack.Push(navView.Content); navigationStack.Push(navView.Content);
selectedNavVieItem = selectedItem; selectedNavVieItem = selectedItem;
break; break;
case "Seasons":
navView.Content = Activator.CreateInstance(typeof(UpcomingPageViewModel));
selectedNavVieItem = selectedItem;
break;
case "Account": case "Account":
navView.Content = Activator.CreateInstance(typeof(AccountPageViewModel)); navView.Content = Activator.CreateInstance(typeof(AccountPageViewModel));
selectedNavVieItem = selectedItem; selectedNavVieItem = selectedItem;
break; break;
case "Settings": case "Settings":
var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel)); var viewModel = (SettingsPageViewModel)Activator.CreateInstance(typeof(SettingsPageViewModel));
viewModel.SetStorageProvider(StorageProvider);
navView.Content = viewModel; navView.Content = viewModel;
selectedNavVieItem = selectedItem; selectedNavVieItem = selectedItem;
break; break;

View file

@ -55,7 +55,7 @@
<RowDefinition Height="Auto" /> <RowDefinition Height="Auto" />
</Grid.RowDefinitions> </Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="50" Text="{Binding SelectedSeries.SeriesTitle}"></TextBlock> <TextBlock Grid.Row="0" FontSize="45" Text="{Binding SelectedSeries.SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
<TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock> <TextBlock Grid.Row="1" FontSize="20" TextWrapping="Wrap" Text="{Binding SelectedSeries.SeriesDescription}"></TextBlock>
<TextBlock Grid.Row="3" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock> <TextBlock Grid.Row="3" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableDubs}"></TextBlock>
<TextBlock Grid.Row="4" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock> <TextBlock Grid.Row="4" FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding AvailableSubs}"></TextBlock>
@ -121,7 +121,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding EditMode}"> IsVisible="{Binding EditMode}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" /> <controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
@ -134,11 +134,56 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel> <StackPanel>
<controls:SettingsExpander Header="Language Override"> <controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" -->
<!-- ItemsSource="{Binding CrunchyrollManager.VideoQualityList}" -->
<!-- SelectedItem="{Binding SelectedSeries.SelectedVideoQuality}"> -->
<!-- </ComboBox> -->
<StackPanel>
<ToggleButton x:Name="OverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSeries.SelectedVideoQualityItem.stringValue, FallbackValue=''}"
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=OverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=OverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SelectedSeries.VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedSeries.SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -175,7 +220,7 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
@ -426,7 +471,7 @@
VerticalAlignment="Center" VerticalAlignment="Center"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).EditMode}"> IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).EditMode}">
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" /> <controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel> </StackPanel>
</ToggleButton> </ToggleButton>
@ -439,11 +484,52 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}"> <Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel> <StackPanel>
<controls:SettingsExpander Header="Language Override"> <controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer> <controls:SettingsExpander.Footer>
<StackPanel> <StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 0 0 10">
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Video Quality" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonQuality" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedVideoQualityItem.stringValue, FallbackValue=''}"
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=SeasonOverrideDropdownButtonQuality, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SeasonOverrideDropdownButtonQuality}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxQulitiesSelection" SelectionMode="Single,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding VideoQualityList , Mode=OneWay}"
SelectedItem="{Binding SelectedVideoQualityItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
</StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right" Margin="0 0 0 10">
<TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Dub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -480,7 +566,7 @@
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center" HorizontalAlignment="Right">
<TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock> <TextBlock Text="Sub" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0 0 10 0"></TextBlock>
<StackPanel> <StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch"> <ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views; namespace CRD.Views;

View file

@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels" xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel" x:DataType="vm:SettingsPageViewModel"
x:Class="CRD.Views.SettingsPageView" x:Class="CRD.Views.SettingsPageView"
@ -14,894 +13,10 @@
<vm:SettingsPageViewModel /> <vm:SettingsPageViewModel />
</Design.DataContext> </Design.DataContext>
<ScrollViewer Padding="20 20 20 0"> <controls:TabView TabItems="{Binding Tabs}"
<StackPanel Spacing="8"> AllowDropTabs="False" IsAddTabButtonVisible="False"
Background="Transparent" CanDragTabs="False" CanReorderTabs="False"
VerticalAlignment="Stretch">
<controls:SettingsExpander Header="Dub language" </controls:TabView>
IconSource="Speaker2"
Description="Change the selected dub language (with multiple dubs some can be out of sync)">
<controls:SettingsExpander.Footer>
<!-- <ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400" -->
<!-- -->
<!-- ItemsSource="{Binding DubLangList}" -->
<!-- SelectedItem="{Binding SelectedDubLang}"> -->
<!-- </ComboBox> -->
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedDubs}"
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=DropdownButtonDub, Mode=TwoWay}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=DropdownButtonDub}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding DubLangList}"
SelectedItems="{Binding SelectedDubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
</StackPanel>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Hardsubs language"
IconSource="FontColorFilled"
Description="Change the selected hardsub language">
<controls:SettingsExpander.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding HardSubLangList}"
SelectedItem="{Binding SelectedHSLang}">
</ComboBox>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Softsubs language"
IconSource="FontColor"
Description="Change the selected softsubs language">
<controls:SettingsExpander.Footer>
<StackPanel>
<ToggleButton x:Name="dropdownButton" Width="210" HorizontalContentAlignment="Stretch">
<ToggleButton.Content>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBlock HorizontalAlignment="Center" Text="{Binding SelectedSubs}"
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=dropdownButton, Mode=TwoWay}" Placement="Bottom"
PlacementTarget="{Binding ElementName=dropdownButton}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="listBoxSubsSelection" SelectionMode="Multiple,Toggle" Width="210"
MaxHeight="400"
ItemsSource="{Binding SubLangList}" SelectedItems="{Binding SelectedSubLang}"
PointerWheelChanged="ListBox_PointerWheelChanged">
</ListBox>
</Border>
</Popup>
</StackPanel>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="Add ScaledBorderAndShadow ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<ComboBox HorizontalContentAlignment="Center" IsVisible="{Binding AddScaledBorderAndShadow}" Margin="5 0" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding ScaledBorderAndShadow}"
SelectedItem="{Binding SelectedScaledBorderAndShadow}">
</ComboBox>
<CheckBox IsChecked="{Binding AddScaledBorderAndShadow}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Signs Subtitles " Description="Download Signs (Forced) Subtitles">
<controls:SettingsExpanderItem.Footer>
<CheckBox HorizontalAlignment="Right" IsChecked="{Binding IncludeSignSubs}"> </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 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 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>
<controls:SettingsExpander Header="History"
IconSource="Clock"
Description="Change if the download history is recorded">
<controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding History}"> </CheckBox>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="History Language" Description="Use the same language as Sonarr if you plan to connect it to this downloader">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding HistoryLangList}"
SelectedItem="{Binding SelectedHistoryLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Add Specials" Description="Add specials to the queue if they weren't downloaded before">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Missing/New Count from Sonarr" Description="The missing count (number in the orange corner) will count the episodes missing from sonarr">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Download Settings"
IconSource="Download"
Description="Adjust download settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Max Download Speed"
Description="Download in Kb/s - 0 is full speed">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="1000000000"
Value="{Binding DownloadSpeed}"
SpinButtonPlacementMode="Hidden"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Stream Endpoint ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding StreamEndpoints}"
SelectedItem="{Binding SelectedStreamEndpoint}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Temp Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock IsVisible="{Binding DownloadToTempFolder}" FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding TempDownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button IsVisible="{Binding DownloadToTempFolder}" Margin="10 0 10 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogTempFolderAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<CheckBox IsChecked="{Binding DownloadToTempFolder}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding DownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Simultaneous Downloads">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="10"
Value="{Binding SimultaneousDownloads}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadVideo}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Video for every dub">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<CheckBox IsChecked="{Binding DownloadVideoForEveryDub}"> </CheckBox>
<CheckBox IsVisible="{Binding DownloadVideoForEveryDub}" Content="Keep files separate" IsChecked="{Binding KeepDubsSeparate}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Video Quality">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding VideoQualityList}"
SelectedItem="{Binding SelectedVideoQuality}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Audio">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadAudio}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Audio Quality">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding AudioQualityList}"
SelectedItem="{Binding SelectedAudioQuality}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Chapters">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding DownloadChapters}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Filename Settings"
IconSource="Edit"
Description="Change how the files are named"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Leading 0 for seasons and episodes">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="5"
Value="{Binding LeadingNumbers}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Filename"
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs} - Folder with \\">
<controls:SettingsExpanderItem.Footer>
<TextBox Name="FileNameTextBox" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FileName}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Muxing Settings"
IconSource="Repair"
Description="MKVMerge and FFMpeg Settings"
IsExpanded="False">
<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 IsVisible="{Binding !SkipMuxing}" Content="Keep Subtitles separate">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SkipSubMux}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Audio ">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultDubLangList}"
SelectedItem="{Binding SelectedDefaultDubLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Default Subtitle ">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Vertical">
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DefaultSubLangList}"
SelectedItem="{Binding SelectedDefaultSubLang}">
</ComboBox>
<CheckBox Content="Forced Display" IsChecked="{Binding DefaultSubForcedDisplay}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<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 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 IsVisible="{Binding !SkipMuxing}" Content="Include Episode description">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding IncludeEpisodeDescription}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Episode description Language">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DescriptionLangList}"
SelectedItem="{Binding SelectedDescriptionLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<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 IsVisible="{Binding !SkipMuxing}" Content="Additional MKVMerge Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox Name="TargetTextBox2" HorizontalAlignment="Left" MinWidth="250"
Text="{Binding MkvMergeOption }">
</TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddMkvMergeParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding MkvMergeOptions}" Margin="0,5" 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].((vm:SettingsPageViewModel)DataContext).RemoveMkvMergeParam}"
CommandParameter="{Binding .}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Additional FFMpeg Options">
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<StackPanel Orientation="Horizontal">
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding FfmpegOption }">
</TextBox>
<Button HorizontalAlignment="Center" Margin="5 0" Command="{Binding AddFfmpegParam}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Add" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<ItemsControl ItemsSource="{Binding FfmpegOptions}" Margin="0,5" 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].((vm:SettingsPageViewModel)DataContext).RemoveFfmpegParam}"
CommandParameter="{Binding .}" />
</StackPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</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>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Sonarr Settings"
IconSource="Globe"
Description="Adjust sonarr settings"
IsEnabled="{Binding History}"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Host">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrHost}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Port">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrPort}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="API Key">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrApiKey}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use SSL">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSsl}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Sonarr Numbering"
Description="Potentially wrong if it couldn't be matched">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSonarrNumbering}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</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">
<controls:SettingsExpander.Footer>
<ComboBox SelectedItem="{Binding CurrentAppTheme}"
ItemsSource="{Binding AppThemes}"
MinWidth="150" />
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="App Accent Color"
IconSource="ColorLine"
Description="Set a custom accent color for the App"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Preview">
<controls:SettingsExpanderItem.Footer>
<Grid RowDefinitions="*,*,*,*"
ColumnDefinitions="*,*"
HorizontalAlignment="Right"
Grid.Column="1">
<Border Background="{DynamicResource SystemAccentColor}"
Height="40" Grid.ColumnSpan="2">
<TextBlock Text="SystemAccentColor"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight1}"
Height="40" Width="90" Grid.Column="0" Grid.Row="1">
<TextBlock Text="Light1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight2}"
Height="40" Width="90" Grid.Column="0" Grid.Row="2">
<TextBlock Text="Light2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight3}"
Height="40" Width="90" Grid.Column="0" Grid.Row="3">
<TextBlock Text="Light3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark1}"
Height="40" Width="90" Grid.Column="1" Grid.Row="1">
<TextBlock Text="Dark1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark2}"
Height="40" Width="90" Grid.Column="1" Grid.Row="2">
<TextBlock Text="Dark2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark3}"
Height="40" Width="90" Grid.Column="1" Grid.Row="3">
<TextBlock Text="Dark3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem>
<CheckBox Content="Use Custom Accent Color?"
IsChecked="{Binding UseCustomAccent}"
HorizontalAlignment="Right" />
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<TextBlock Text="Pre-set Colors"
Margin="24 24 0 0"
IsVisible="{Binding UseCustomAccent}" />
<ListBox ItemsSource="{Binding PredefinedColors}"
SelectedItem="{Binding ListBoxColor}"
MaxWidth="441"
AutoScrollToSelectedItem="False"
Margin="24 0 24 12"
HorizontalAlignment="Left"
IsVisible="{Binding UseCustomAccent}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Margin" Value="1 1 0 0" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border CornerRadius="{StaticResource ControlCornerRadius}"
BorderThickness="2"
Name="Root">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
<Border Name="Check"
Background="{DynamicResource FocusStrokeColorOuter}"
Width="20" Height="20"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0 2 2 0">
<controls:SymbolIcon Symbol="Checkmark"
Foreground="{DynamicResource SystemAccentColor}"
FontSize="18" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ListBoxItem /template/ Border#Check">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#Check">
<Setter Property="IsVisible" Value="True" />
</Style>
</ListBox.Styles>
</ListBox>
<Rectangle Fill="{DynamicResource ApplicationPageBackgroundThemeBrush}"
Height="1"
IsVisible="{Binding UseCustomAccent}" />
<DockPanel LastChildFill="False" Margin="24 6 0 0"
IsVisible="{Binding UseCustomAccent}">
<TextBlock Text="Custom Color"
VerticalAlignment="Center"
DockPanel.Dock="Left" />
<controls:ColorPickerButton Color="{Binding CustomAccentColor}"
IsMoreButtonVisible="True"
UseSpectrum="True"
UseColorWheel="False"
UseColorTriangle="False"
UseColorPalette="False"
IsCompact="True" ShowAcceptDismissButtons="True"
DockPanel.Dock="Right" />
</DockPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Log Mode"
IconSource="Help"
Description="Should only be enabled if something isn't working">
<controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding LogMode}"> </CheckBox>
</controls:SettingsExpander.Footer>
</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">
<DockPanel HorizontalAlignment="Center">
<Image Source="/Assets/app_icon.ico"
DockPanel.Dock="Left"
Height="78"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<StackPanel Spacing="0" Margin="12 0">
<TextBlock Text="Crunchy-Downloader"
Theme="{StaticResource TitleTextBlockStyle}" />
<TextBlock Text="{Binding CurrentVersion}"
Theme="{StaticResource BodyTextBlockStyle}" />
<TextBlock Theme="{StaticResource CaptionTextBlockStyle}"
Text="https://github.com/Crunchy-DL/Crunchy-Downloader"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</DockPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl> </UserControl>

View file

@ -1,12 +1,7 @@
using System.Linq; using System.Linq;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.VisualTree; using Avalonia.VisualTree;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Sonarr; using CRD.Utils.Sonarr;
using CRD.ViewModels; using CRD.ViewModels;

View file

@ -1,5 +1,4 @@
using System; using System;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.Threading; using Avalonia.Threading;

View file

@ -0,0 +1,195 @@
<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:vm="clr-namespace:CRD.ViewModels"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
x:DataType="vm:UpcomingPageViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.UpcomingPageView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Top" Margin="10">
<ItemsControl ItemsSource="{Binding Seasons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type structs:SeasonViewModel}">
<Button
Width="120"
Height="50"
HorizontalContentAlignment="Center"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).SelectSeasonCommand}"
CommandParameter="{Binding}"
Margin="5">
<Button.Content>
<StackPanel Orientation="Vertical" VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock IsVisible="{Binding IsSelected}" Text="{Binding Season}" FontSize="16" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock IsVisible="{Binding !IsSelected}" Foreground="Gray" Text="{Binding Season}" FontSize="16" FontWeight="Bold" HorizontalAlignment="Center" />
<TextBlock Text="{Binding Year}" FontSize="12" HorizontalAlignment="Center" />
</StackPanel>
</Button.Content>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
<ListBox Grid.Row="1" IsVisible="{Binding !IsLoading}" ItemsSource="{Binding SelectedSeason}"
SelectedItem="{Binding SelectedSeries, Mode=TwoWay}" SelectedIndex="{Binding SelectedIndex}" SelectionChanged="SelectionChanged"
Margin="5">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<!-- <ListBox.Styles> -->
<!-- <Style Selector="ListBoxItem:selected /template/ ContentPresenter"> -->
<!-- <Setter Property="Background" Value="Transparent"/> -->
<!-- </Style> -->
<!-- <Style Selector="ListBoxItem:selected /template/ Rectangle"> -->
<!-- <Setter Property="IsVisible" Value="False"/> -->
<!-- </Style> -->
<!-- </ListBox.Styles> -->
<ListBox.ItemTemplate>
<DataTemplate>
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="185" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0" Orientation="Vertical" HorizontalAlignment="Center"
Width="185"
Height="315"
Margin="5">
<Grid>
<Image Source="{Binding ThumbnailImage}"
Width="185"
Height="265">
</Image>
<StackPanel VerticalAlignment="Top" HorizontalAlignment="Right"
IsVisible="{Binding IsInHistory}" Margin="0 5 5 5" >
<Border Background="DarkGray" CornerRadius="50">
<controls:SymbolIcon Symbol="Library" Foreground="Black" FontSize="22" Margin="2" />
<ToolTip.Tip>
<TextBlock Text="Series is in History" FontSize="15" />
</ToolTip.Tip>
</Border>
</StackPanel>
</Grid>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding Title.English}"
TextWrapping="Wrap"
Width="185"
FontSize="12"
Height="35"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding Title.English}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding StartDateForm}"
TextWrapping="Wrap"
Width="185"
FontSize="12"
MaxHeight="20"
Margin="4,0,4,0">
<ToolTip.Tip>
<TextBlock Text="{Binding StartDateForm}" FontSize="15" />
</ToolTip.Tip>
</TextBlock>
</StackPanel>
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" >
<Expander.Styles>
<Style Selector="Expander:not(:expanded) /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="Expander:expanded /template/ ToggleButton#ExpanderHeader">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="ToggleButton:pointerover /template/ Border#ExpandCollapseChevronBorder">
<Setter Property="Background" Value="Transparent" />
</Style>
<Style Selector="ToggleButton:not(:checked) /template/ TextBlock#ExpandCollapseChevron">
<Setter Property="Foreground" Value="Transparent" />
</Style>
<Style Selector="ToggleButton:checked /template/ TextBlock#ExpandCollapseChevron">
<Setter Property="Foreground" Value="Transparent" />
</Style>
</Expander.Styles>
<Expander.Header>
<Border Width="117" Height="315" />
</Expander.Header>
<Expander.Content>
<StackPanel>
<ScrollViewer MaxHeight="265" MinHeight="265" PointerWheelChanged="ScrollViewer_PointerWheelChanged" Margin="0 0 0 5">
<TextBlock HorizontalAlignment="Center" TextAlignment="Center"
Text="{Binding Description}"
TextWrapping="Wrap"
Width="185"
FontSize="16"
Margin="5">
</TextBlock>
</ScrollViewer>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Bottom">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom" Content="Trailer" Margin=" 0 0 5 0"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).OpenTrailer}"
CommandParameter="{Binding}"></Button>
<StackPanel IsVisible="{Binding HasCrID}">
<Button HorizontalAlignment="Right" VerticalAlignment="Bottom"
IsVisible="{Binding !IsInHistory}"
Command="{Binding $parent[UserControl].((vm:UpcomingPageViewModel)DataContext).AddToHistory}"
CommandParameter="{Binding}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Library" FontSize="20" />
<controls:SymbolIcon Symbol="Add" FontSize="20" />
</StackPanel>
</Button>
</StackPanel>
</StackPanel>
</StackPanel>
</Expander.Content>
</Expander>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<controls:ProgressRing Grid.Row="1" IsVisible="{Binding IsLoading}" VerticalAlignment="Center" HorizontalAlignment="Center" Width="100" Height="100"></controls:ProgressRing>
</Grid>
</UserControl>

View file

@ -0,0 +1,34 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.ViewModels;
namespace CRD.Views;
public partial class UpcomingPageView : UserControl{
public UpcomingPageView(){
InitializeComponent();
}
private void SelectionChanged(object? sender, SelectionChangedEventArgs e){
if (DataContext is UpcomingPageViewModel viewModel && sender is ListBox listBox){
viewModel.SelectionChangedOfSeries((AnilistSeries?)listBox.SelectedItem);
}
}
private void ScrollViewer_PointerWheelChanged(object sender, Avalonia.Input.PointerWheelEventArgs e){
if (sender is ScrollViewer scrollViewer){
// 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,41 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:utils="clr-namespace:CRD.ViewModels.Utils"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
x:DataType="utils:ContentDialogDropdownSelectViewModel"
x:Class="CRD.Views.Utils.ContentDialogDropdownSelectView">
<StackPanel Spacing="10" MinWidth="400">
<TextBlock Text="{Binding EpisodeInfo}"></TextBlock>
<ComboBox HorizontalAlignment="Center" HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding DropDownItemList}"
SelectedItem="{Binding SelectedDropdownItem}">
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:StringItem}">
<TextBlock Text="{Binding stringValue}"></TextBlock>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<!-- <ListBox SelectionMode="Single" Width="210" -->
<!-- MaxHeight="400" -->
<!-- ItemsSource="{Binding DropDownItemList , Mode=OneWay}" -->
<!-- SelectedItems="{Binding SelectedDropdownItem}"> -->
<!-- <ListBox.ItemTemplate> -->
<!-- <DataTemplate DataType="{x:Type structs:StringItem}"> -->
<!-- <TextBlock Text="{Binding stringValue}"></TextBlock> -->
<!-- </DataTemplate> -->
<!-- </ListBox.ItemTemplate> -->
<!-- </ListBox> -->
</StackPanel>
</UserControl>

View file

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

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views.Utils; namespace CRD.Views.Utils;

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views.Utils; namespace CRD.Views.Utils;

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views.Utils; namespace CRD.Views.Utils;

View file

@ -1,6 +1,4 @@
using Avalonia; using Avalonia.Controls;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace CRD.Views.Utils; namespace CRD.Views.Utils;

View file

@ -0,0 +1,516 @@
<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:vm="clr-namespace:CRD.ViewModels.Utils"
x:DataType="vm:GeneralSettingsViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="CRD.Views.Utils.GeneralSettingsView">
<Design.DataContext>
<vm:GeneralSettingsViewModel />
</Design.DataContext>
<ScrollViewer Padding="20 20 20 0">
<StackPanel Spacing="8">
<controls:SettingsExpander Header="History"
IconSource="Clock"
Description="Change if the download history is recorded">
<controls:SettingsExpander.Footer>
<CheckBox IsChecked="{Binding History}"> </CheckBox>
</controls:SettingsExpander.Footer>
<controls:SettingsExpanderItem Content="History Language" Description="Use the same language as Sonarr if you plan to connect it to this downloader">
<controls:SettingsExpanderItem.Footer>
<ComboBox HorizontalContentAlignment="Center" MinWidth="210" MaxDropDownHeight="400"
ItemsSource="{Binding HistoryLangList}"
SelectedItem="{Binding SelectedHistoryLang}">
</ComboBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Add Specials" Description="Add specials to the queue if they weren't downloaded before">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryAddSpecials}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="History Missing/New Count from Sonarr" Description="The missing count (number in the orange corner) will count the episodes missing from sonarr">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding HistoryCountSonarr}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Download Settings"
IconSource="Download"
Description="Adjust download settings"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Max Download Speed"
Description="Download in Kb/s - 0 is full speed">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="1000000000"
Value="{Binding DownloadSpeed}"
SpinButtonPlacementMode="Hidden"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Temp Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock IsVisible="{Binding DownloadToTempFolder}" FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding TempDownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button IsVisible="{Binding DownloadToTempFolder}" Margin="10 0 10 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogTempFolderAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Temp Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<Button IsVisible="{Binding DownloadToTempFolder}" Margin="0 0 10 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding ClearDownloadTempDirPath}">
<ToolTip.Tip>
<TextBlock Text="Reset to default" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Clear" FontSize="18" />
</StackPanel>
</Button>
<CheckBox IsChecked="{Binding DownloadToTempFolder}"> </CheckBox>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Download Folder">
<controls:SettingsExpanderItem.Footer>
<StackPanel Orientation="Horizontal">
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="NoWrap" Text="{Binding DownloadDirPath, Mode=OneWay}" TextAlignment="Center" VerticalAlignment="Center" HorizontalAlignment="Center" />
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding OpenFolderDialogAsync}">
<ToolTip.Tip>
<TextBlock Text="Set Download Directory" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<Button Margin="10 0 0 0" FontStyle="Italic"
VerticalAlignment="Center"
Command="{Binding ClearDownloadDirPath}">
<ToolTip.Tip>
<TextBlock Text="Reset to default" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Clear" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Simultaneous Downloads">
<controls:SettingsExpanderItem.Footer>
<controls:NumberBox Minimum="0" Maximum="10"
Value="{Binding SimultaneousDownloads}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpander.Footer>
</controls:SettingsExpander.Footer>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Sonarr Settings"
IconSource="Globe"
Description="Adjust Sonarr settings"
IsEnabled="{Binding History}"
IsExpanded="False">
<controls:SettingsExpanderItem Content="Host">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrHost}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Port">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrPort}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="API Key">
<controls:SettingsExpanderItem.Footer>
<TextBox HorizontalAlignment="Left" MinWidth="250"
Text="{Binding SonarrApiKey}" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use SSL">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSsl}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Use Sonarr Numbering"
Description="May be incorrect if unable to match episodes">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding SonarrUseSonarrNumbering}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Proxy Settings"
IconSource="Wifi3"
Description="Changes will take effect after a restart"
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 Appearance"
IconSource="DarkTheme"
Description="Customize the look and feel of the application"
IsExpanded="False">
<controls:SettingsExpanderItem Content="App Theme" Description="Select the theme for the application (Light, Dark, System)">
<controls:SettingsExpanderItem.Footer>
<ComboBox SelectedItem="{Binding CurrentAppTheme}"
ItemsSource="{Binding AppThemes}"
MinWidth="150" />
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="Background image">
<controls:SettingsExpanderItem.Footer>
<StackPanel Spacing="10">
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<TextBlock Text="{Binding BackgroundImagePath, Mode=OneWay}"
FontSize="15"
Opacity="0.8"
TextWrapping="NoWrap"
TextAlignment="Center"
VerticalAlignment="Center" />
<Button Command="{Binding OpenImageFileDialogAsyncInternalBackgroundImage}"
VerticalAlignment="Center"
FontStyle="Italic">
<ToolTip.Tip>
<TextBlock Text="Select Background Image" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal" Spacing="5">
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
</StackPanel>
</Button>
<Button Command="{Binding ClearBackgroundImagePath}"
VerticalAlignment="Center"
FontStyle="Italic">
<ToolTip.Tip>
<TextBlock Text="Remove Background Image Path" FontSize="15" />
</ToolTip.Tip>
<StackPanel Orientation="Horizontal" Spacing="5">
<controls:SymbolIcon Symbol="Clear" FontSize="18" />
</StackPanel>
</Button>
</StackPanel>
<Grid HorizontalAlignment="Right" Margin="0 5 0 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="150" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Opacity"
FontSize="15"
Opacity="0.8"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Margin="0 0 5 10"
Grid.Row="0" Grid.Column="0" />
<controls:NumberBox Minimum="0" Maximum="1"
SmallChange="0.05"
LargeChange="0.1"
SimpleNumberFormat="F2"
Value="{Binding BackgroundImageOpacity}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch"
Margin="0 0 0 10"
Grid.Row="0" Grid.Column="1" />
<TextBlock Text="Blur Radius"
FontSize="15"
Opacity="0.8"
VerticalAlignment="Center"
HorizontalAlignment="Right"
Margin="0 0 5 0"
Grid.Row="1" Grid.Column="0" />
<controls:NumberBox Minimum="0" Maximum="40"
SmallChange="1"
LargeChange="5"
SimpleNumberFormat="F0"
Value="{Binding BackgroundImageBlurRadius}"
SpinButtonPlacementMode="Inline"
HorizontalAlignment="Stretch"
Grid.Row="1" Grid.Column="1" />
</Grid>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="App accent color preview" Description="Choose a custom accent color for the application">
<controls:SettingsExpanderItem.Footer>
<Grid RowDefinitions="*,*,*,*"
ColumnDefinitions="*,*"
HorizontalAlignment="Right"
Grid.Column="1">
<Border Background="{DynamicResource SystemAccentColor}"
Height="40" Grid.ColumnSpan="2">
<TextBlock Text="SystemAccentColor"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight1}"
Height="40" Width="90" Grid.Column="0" Grid.Row="1">
<TextBlock Text="Light1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight2}"
Height="40" Width="90" Grid.Column="0" Grid.Row="2">
<TextBlock Text="Light2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorLight3}"
Height="40" Width="90" Grid.Column="0" Grid.Row="3">
<TextBlock Text="Light3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark1}"
Height="40" Width="90" Grid.Column="1" Grid.Row="1">
<TextBlock Text="Dark1"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark2}"
Height="40" Width="90" Grid.Column="1" Grid.Row="2">
<TextBlock Text="Dark2"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
<Border Background="{DynamicResource SystemAccentColorDark3}"
Height="40" Width="90" Grid.Column="1" Grid.Row="3">
<TextBlock Text="Dark3"
Foreground="{DynamicResource TextOnAccentFillColorPrimaryBrush}"
HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</Grid>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem>
<CheckBox Content="Use Custom Accent Color?"
IsChecked="{Binding UseCustomAccent}"
HorizontalAlignment="Right" />
<controls:SettingsExpanderItem.Footer>
<StackPanel>
<TextBlock Text="Pre-set Colors"
Margin="24 24 0 0"
IsVisible="{Binding UseCustomAccent}" />
<ListBox ItemsSource="{Binding PredefinedColors}"
SelectedItem="{Binding ListBoxColor}"
MaxWidth="441"
AutoScrollToSelectedItem="False"
Margin="24 0 24 12"
HorizontalAlignment="Left"
IsVisible="{Binding UseCustomAccent}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Width" Value="48" />
<Setter Property="Height" Value="48" />
<Setter Property="MinWidth" Value="0" />
<Setter Property="Margin" Value="1 1 0 0" />
<Setter Property="Template">
<ControlTemplate>
<Panel>
<Border CornerRadius="{StaticResource ControlCornerRadius}"
BorderThickness="2"
Name="Root">
<Border.Background>
<SolidColorBrush Color="{Binding}" />
</Border.Background>
</Border>
<Border Name="Check"
Background="{DynamicResource FocusStrokeColorOuter}"
Width="20" Height="20"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="0 2 2 0">
<controls:SymbolIcon Symbol="Checkmark"
Foreground="{DynamicResource SystemAccentColor}"
FontSize="18" />
</Border>
</Panel>
</ControlTemplate>
</Setter>
</Style>
<Style Selector="ListBoxItem /template/ Border#Check">
<Setter Property="IsVisible" Value="False" />
</Style>
<Style Selector="ListBoxItem:pointerover /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#Root">
<Setter Property="BorderBrush" Value="{DynamicResource FocusStrokeColorOuter}" />
</Style>
<Style Selector="ListBoxItem:selected /template/ Border#Check">
<Setter Property="IsVisible" Value="True" />
</Style>
</ListBox.Styles>
</ListBox>
<Rectangle Fill="{DynamicResource ApplicationPageBackgroundThemeBrush}"
Height="1"
IsVisible="{Binding UseCustomAccent}" />
<DockPanel LastChildFill="False" Margin="24 6 0 0"
IsVisible="{Binding UseCustomAccent}">
<TextBlock Text="Custom Color"
VerticalAlignment="Center"
DockPanel.Dock="Left" />
<controls:ColorPickerButton Color="{Binding CustomAccentColor}"
IsMoreButtonVisible="True"
UseSpectrum="True"
UseColorWheel="False"
UseColorTriangle="False"
UseColorPalette="False"
IsCompact="True" ShowAcceptDismissButtons="True"
DockPanel.Dock="Right" />
</DockPanel>
</StackPanel>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<controls:SettingsExpander Header="Debug"
IconSource="Help"
Description="Tools and options for debugging and troubleshooting issues">
<controls:SettingsExpanderItem Content="Log Mode" Description="Enable error logging. Recommended only for troubleshooting issues">
<controls:SettingsExpanderItem.Footer>
<CheckBox IsChecked="{Binding LogMode}"> </CheckBox>
</controls:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
<controls:SettingsExpanderItem Content="IP" Description="Check your current IP address to confirm if traffic is routed through a VPN">
<controls:SettingsExpanderItem.Footer>
<Grid VerticalAlignment="Center">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Border VerticalAlignment="Center" Height="30">
<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:SettingsExpanderItem.Footer>
</controls:SettingsExpanderItem>
</controls:SettingsExpander>
<Grid Margin="0 0 0 10"
ColumnDefinitions="*,Auto" RowDefinitions="*,Auto">
<DockPanel HorizontalAlignment="Center">
<Image Source="/Assets/app_icon.ico"
DockPanel.Dock="Left"
Height="78"
RenderOptions.BitmapInterpolationMode="HighQuality" />
<StackPanel Spacing="0" Margin="12 0">
<TextBlock Text="Crunchy-Downloader"
Theme="{StaticResource TitleTextBlockStyle}" />
<TextBlock Text="{Binding CurrentVersion}"
Theme="{StaticResource BodyTextBlockStyle}" />
<TextBlock Theme="{StaticResource CaptionTextBlockStyle}"
Text="https://github.com/Crunchy-DL/Crunchy-Downloader"
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
</StackPanel>
</DockPanel>
</Grid>
</StackPanel>
</ScrollViewer>
</UserControl>

View file

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

View file

@ -15,4 +15,11 @@
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" /> <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application> </application>
</compatibility> </compatibility>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings xmlns:ws2="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
<ws2:longPathAware>true</ws2:longPathAware>
</windowsSettings>
</application>
</assembly> </assembly>