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.IO;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@ -7,8 +6,6 @@ using CRD.ViewModels;
using MainWindow = CRD.Views.MainWindow;
using System.Linq;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Updater;
namespace CRD;

View file

@ -4,11 +4,16 @@ using System.Globalization;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using DynamicData;
using HtmlAgilityPack;
using Newtonsoft.Json;
namespace CRD.Downloader;
@ -87,7 +92,7 @@ public class CalendarManager{
var date = day.SelectSingleNode(".//time[@datetime]")?.GetAttributeValue("datetime", "No date");
DateTime dayDateTime = DateTime.Parse(date, null, DateTimeStyles.RoundtripKind);
if (week.FirstDayOfWeek == null){
if (week.FirstDayOfWeek == DateTime.MinValue){
week.FirstDayOfWeek = dayDateTime;
week.FirstDayOfWeekString = dayDateTime.ToString("yyyy-MM-dd");
}
@ -147,8 +152,10 @@ public class CalendarManager{
}
public async Task<CalendarWeek> BuildCustomCalendar(bool forceUpdate){
if (!forceUpdate && calendar.TryGetValue("C" + DateTime.Now.ToString("yyyy-MM-dd"), out var forDate)){
public async Task<CalendarWeek> BuildCustomCalendar(DateTime calTargetDate, bool forceUpdate){
await LoadAnilistUpcoming();
if (!forceUpdate && calendar.TryGetValue("C" + calTargetDate.ToString("yyyy-MM-dd"), out var forDate)){
return forDate;
}
@ -156,14 +163,14 @@ public class CalendarManager{
CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>();
DateTime today = DateTime.Now;
DateTime targetDay = calTargetDate;
for (int i = 0; i < 7; i++){
CalendarDay calDay = new CalendarDay();
calDay.CalendarEpisodes = new List<CalendarEpisode>();
calDay.DateTime = today.AddDays(-i);
calDay.DayName = calDay.DateTime.Value.DayOfWeek.ToString();
calDay.DateTime = targetDay.AddDays(-i);
calDay.DayName = calDay.DateTime.DayOfWeek.ToString();
week.CalendarDays.Add(calDay);
}
@ -171,21 +178,68 @@ public class CalendarManager{
week.CalendarDays.Reverse();
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 }){
var newEpisodes = newEpisodesBase.Data;
//EpisodeAirDate
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 &&
(crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)") || crBrowseEpisode.EpisodeMetadata.AudioLocale != Locale.JaJp)){
continue;
DateTime premiumAvailableStart = crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.Kind == DateTimeKind.Utc
? crBrowseEpisode.EpisodeMetadata.PremiumAvailableDate.ToLocalTime()
: 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;
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 (crBrowseEpisode.EpisodeMetadata.AudioLocale != null && crBrowseEpisode.EpisodeMetadata.AudioLocale.GetEnumMemberValue() != dubFilter){
continue;
@ -193,7 +247,7 @@ public class CalendarManager{
}
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();
if (calendarDay != null){
@ -209,13 +263,62 @@ public class CalendarManager{
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
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){
if (weekCalendarDay.CalendarEpisodes != null)
if (weekCalendarDay.CalendarEpisodes.Count > 0)
weekCalendarDay.CalendarEpisodes = weekCalendarDay.CalendarEpisodes
.OrderBy(e => e.DateTime)
.ThenBy(e => e.SeasonName)
@ -232,9 +335,232 @@ public class CalendarManager{
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;
}
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.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading.Tasks;
using CRD.Utils;
using CRD.Utils.Structs;
@ -17,40 +16,51 @@ public class CrAuth{
private readonly CrunchyrollManager crunInstance = CrunchyrollManager.Instance;
public async Task AuthAnonymous(){
string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{
{ "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
};
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);
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{
Console.Error.WriteLine("Anonymous login failed");
}
crunInstance.Profile = new CrProfile{
Username = "???",
Avatar = "003-cr-hime-excited.png",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE"
};
}
private void JsonTokenToFileAndVariable(string content){
private void JsonTokenToFileAndVariable(string content, string deviceId){
crunInstance.Token = Helpers.Deserialize<CrToken>(content, crunInstance.SettingsJsonSerializerSettings);
if (crunInstance.Token != null && crunInstance.Token.expires_in != null){
if (crunInstance.Token is{ expires_in: not null }){
crunInstance.Token.device_id = deviceId;
crunInstance.Token.expires = DateTime.Now.AddSeconds((double)crunInstance.Token.expires_in);
CfgManager.WriteTokenToYamlFile(crunInstance.Token, CfgManager.PathCrToken);
@ -58,25 +68,36 @@ public class CrAuth{
}
public async Task Auth(AuthData data){
var formData = new Dictionary<string, string>{
{ "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");
string uuid = Guid.NewGuid().ToString();
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
};
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);
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{
if (response.ResponseContent.Contains("invalid_credentials")){
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10));
@ -99,7 +120,7 @@ public class CrAuth{
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);
@ -109,7 +130,7 @@ public class CrAuth{
if (profileTemp != null){
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);
@ -152,35 +173,50 @@ public class CrAuth{
public async Task LoginWithToken(){
if (crunInstance.Token?.refresh_token == null){
Console.Error.WriteLine("Missing Refresh Token");
await AuthAnonymous();
return;
}
string uuid = Guid.NewGuid().ToString();
var formData = new Dictionary<string, string>{
{ "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
};
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);
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{
Console.Error.WriteLine("Token Auth Failed");
}
if (crunInstance.Token?.refresh_token != null){
HttpClientReq.Instance.SetETPCookie(crunInstance.Token.refresh_token);
await GetProfile();
await AuthAnonymous();
}
}
@ -198,24 +234,37 @@ public class CrAuth{
return;
}
var formData = new Dictionary<string, string>{
{ "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");
string uuid = Guid.NewGuid().ToString();
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
};
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);
if (response.IsOk){
JsonTokenToFileAndVariable(response.ResponseContent);
JsonTokenToFileAndVariable(response.ResponseContent, uuid);
} else{
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);
@ -181,6 +181,7 @@ public class CrEpisode(){
};
epMeta.AvailableSubs = item.SubtitleLocales;
epMeta.Description = item.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
if (episodeP.EpisodeAndLanguages.Langs.Count > 0){
epMeta.SelectedDubs = dubLang
@ -236,7 +237,7 @@ public class CrEpisode(){
query["sort_by"] = "newly_added";
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);

View file

@ -3,7 +3,6 @@ using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using System.Web;
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);
@ -81,6 +80,7 @@ public class CrMovies{
};
epMeta.AvailableSubs = new List<string>();
epMeta.Description = episodeP.Description;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
return epMeta;
}

View file

@ -23,8 +23,8 @@ public class CrMusic{
}
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 concertsTask = FetchMediaListAsync($"{Api.Content}/music/artists/{id}/concerts", crLocale, forcedLang);
var musicVideosTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/music_videos", crLocale, forcedLang);
var concertsTask = FetchMediaListAsync($"{ApiUrls.Content}/music/artists/{id}/concerts", crLocale, forcedLang);
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){
var mediaList = await FetchMediaListAsync($"{Api.Content}/{endpoint}/{id}", crLocale, forcedLang);
var mediaList = await FetchMediaListAsync($"{ApiUrls.Content}/{endpoint}/{id}", crLocale, forcedLang);
switch (mediaList.Total){
case < 1:
@ -110,6 +110,7 @@ public class CrMusic{
epMeta.AvailableSubs = new List<string>();
epMeta.Description = episodeP.Description;
epMeta.Music = true;
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
return epMeta;
}

View file

@ -77,6 +77,7 @@ public class CrSeries(){
Time = 0,
DownloadSpeed = 0
};
epMeta.Hslang = CrunchyrollManager.Instance.CrunOptions.Hslang;
epMeta.Description = item.Description;
epMeta.AvailableSubs = item.SubtitleLocales;
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);
@ -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);
@ -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);
@ -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);
@ -457,7 +458,7 @@ public class CrSeries(){
query["n"] = "6";
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);
@ -468,6 +469,20 @@ public class CrSeries(){
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;
}
@ -488,7 +503,7 @@ public class CrSeries(){
query["n"] = "50";
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);

View file

@ -5,12 +5,12 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Xml;
using Avalonia.Media;
using CRD.Utils;
using CRD.Utils.DRM;
using CRD.Utils.Ffmpeg_Encoding;
@ -21,7 +21,10 @@ using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using CRD.Utils.Structs.History;
using CRD.ViewModels.Utils;
using CRD.Views;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using LanguageItem = CRD.Utils.Structs.LanguageItem;
@ -116,11 +119,15 @@ public class CrunchyrollManager{
options.Theme = "System";
options.SelectedCalendarLanguage = "en-us";
options.CalendarDubFilter = "none";
options.CustomCalendar = true;
options.DlVideoOnce = true;
options.StreamEndpoint = "web/firefox";
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
options.HistoryLang = DefaultLocale;
options.BackgroundImageOpacity = 0.5;
options.BackgroundImageBlurRadius = 10;
options.History = true;
CfgManager.UpdateSettingsFromFile(options);
@ -141,7 +148,7 @@ public class CrunchyrollManager{
Profile = new CrProfile{
Username = "???",
Avatar = "003-cr-hime-excited.png",
Avatar = "crbrand_avatars_logo_marks_mangagirl_taupe.png",
PreferredContentAudioLanguage = "ja-JP",
PreferredContentSubtitleLanguage = "de-DE",
HasPremium = false,
@ -235,6 +242,8 @@ public class CrunchyrollManager{
}
if (options.SkipMuxing == false){
bool syncError = false;
data.DownloadProgress = new DownloadProgress(){
IsDownloading = true,
Percent = 100,
@ -275,6 +284,10 @@ public class CrunchyrollManager{
if (result is{ merger: not null, isMuxed: true }){
mergers.Add(result.merger);
}
if (result.syncError){
syncError = true;
}
}
foreach (var merger in mergers){
@ -291,7 +304,7 @@ public class CrunchyrollManager{
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){
@ -319,6 +332,8 @@ public class CrunchyrollManager{
},
fileNameAndPath);
syncError = result.syncError;
if (result is{ merger: not null, isMuxed: true }){
result.merger.CleanUp();
}
@ -334,7 +349,7 @@ public class CrunchyrollManager{
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){
@ -349,10 +364,10 @@ public class CrunchyrollManager{
Percent = 100,
Time = 0,
DownloadSpeed = 0,
Doing = "Done"
Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "")
};
if (CrunOptions.RemoveFinishedDownload){
if (CrunOptions.RemoveFinishedDownload && !syncError){
QueueManager.Instance.Queue.Remove(data);
}
} else{
@ -402,7 +417,6 @@ public class CrunchyrollManager{
data.DownloadProgress = new DownloadProgress{
IsDownloading = true,
Done = true,
Percent = 100,
Time = 0,
DownloadSpeed = 0,
@ -471,7 +485,7 @@ public class CrunchyrollManager{
#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;
if (options.Novids == true || data.FindAll(a => a.Type == DownloadMediaType.Video).Count == 0){
@ -480,7 +494,7 @@ public class CrunchyrollManager{
muxToMp3 = true;
} else{
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(),
Output = $"{filename}.{(muxToMp3 ? "mp3" : options.Mp4 ? "mp4" : "mkv")}",
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,
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(),
VideoTitle = options.VideoTitle,
Options = new MuxOptions(){
@ -549,7 +563,7 @@ public class CrunchyrollManager{
Console.Error.WriteLine("MKVmerge not found");
}
bool isMuxed;
bool isMuxed, syncError = false;
if (options.SyncTiming && CrunOptions.DlVideoOnce){
var basePath = merger.options.OnlyVid.First().Path;
@ -559,6 +573,12 @@ public class CrunchyrollManager{
foreach (var syncVideo in syncVideosList){
if (!string.IsNullOrEmpty(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);
if (audio != null){
audio.Delay = (int)(delay * 1000);
@ -585,21 +605,10 @@ public class CrunchyrollManager{
isMuxed = true;
}
return (merger, isMuxed);
return (merger, isMuxed, syncError);
}
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 == "???"){
MainWindow.Instance.ShowError("User Account not recognized - are you signed in?");
return new DownloadResponse{
@ -610,31 +619,55 @@ public class CrunchyrollManager{
};
}
if (!File.Exists(CfgManager.PathFFMPEG)){
Console.Error.WriteLine("Missing ffmpeg");
MainWindow.Instance.ShowError("FFmpeg not found");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Missing ffmpeg"
};
}
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)){
if (!File.Exists(CfgManager.PathFFMPEG)){
Console.Error.WriteLine("Missing ffmpeg");
MainWindow.Instance.ShowError($"FFmpeg not found at: {CfgManager.PathFFMPEG}");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Missing ffmpeg"
};
}
if (!File.Exists(CfgManager.PathMKVMERGE)){
Console.Error.WriteLine("Missing Mkvmerge");
MainWindow.Instance.ShowError("Mkvmerge not found");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
ErrorText = "Missing Mkvmerge"
};
if (!File.Exists(CfgManager.PathMKVMERGE)){
Console.Error.WriteLine("Missing Mkvmerge");
MainWindow.Instance.ShowError($"Mkvmerge not found at: {CfgManager.PathMKVMERGE}");
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = true,
FileName = "./unknown",
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){
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{
Data = new List<DownloadedMedia>(),
Error = true,
@ -645,7 +678,7 @@ public class CrunchyrollManager{
if (!File.Exists(CfgManager.PathMP4Decrypt)){
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{
Data = new List<DownloadedMedia>(),
Error = true,
@ -864,14 +897,52 @@ public class CrunchyrollManager{
return true;
}).ToList();
} else{
dlFailed = true;
if (hsLangs.Count > 0){
var dialog = new ContentDialog(){
Title = "Hardsub Select",
PrimaryButtonText = "Select",
CloseButtonText = "Close"
};
return new DownloadResponse{
Data = new List<DownloadedMedia>(),
Error = dlFailed,
FileName = "./unknown",
ErrorText = "Hardsubs not available"
};
var viewModel = new ContentDialogDropdownSelectViewModel(dialog,
data.SeriesTitle + (!string.IsNullOrEmpty(data.Season)
? " - S" + data.Season + "E" + (data.EpisodeNumber != string.Empty ? data.EpisodeNumber : data.AbsolutEpisodeNumberE)
: "") + " - " +
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{
@ -986,12 +1057,12 @@ public class CrunchyrollManager{
int chosenVideoQuality;
if (options.DlVideoOnce && dlVideoOnce && options.SyncTiming){
chosenVideoQuality = 1;
} else if (options.QualityVideo == "best"){
} else if (data.VideoQuality == "best"){
chosenVideoQuality = videos.Count;
} else if (options.QualityVideo == "worst"){
} else if (data.VideoQuality == "worst"){
chosenVideoQuality = 1;
} 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){
chosenVideoQuality = videos.Count;
} else{
@ -1049,6 +1120,7 @@ public class CrunchyrollManager{
variables.Add(new Variable("height", chosenVideoSegments.quality.height, 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);
if (lang == null){
@ -1070,7 +1142,37 @@ public class CrunchyrollManager{
fileName = Path.Combine(FileNameManager.ParseFileName(options.FileName, variables, options.Numbers, options.Override).ToArray());
string outFile = Path.Combine(FileNameManager.ParseFileName(options.FileName + "." + (epMeta.Lang?.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)
.ToArray());
@ -1163,7 +1265,7 @@ public class CrunchyrollManager{
var json = JsonConvert.SerializeObject(reqBodyData);
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;
var decRequestResponse = await HttpClientReq.Instance.SendHttpRequest(decRequest);
@ -1270,6 +1372,7 @@ public class CrunchyrollManager{
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
} else{
Console.WriteLine("No Video downloaded");
}
@ -1335,6 +1438,7 @@ public class CrunchyrollManager{
Lang = lang.Value,
IsPrimary = isPrimary
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
} else{
Console.WriteLine("No Audio downloaded");
}
@ -1351,6 +1455,7 @@ public class CrunchyrollManager{
IsPrimary = isPrimary
};
files.Add(videoDownloadMedia);
data.downloadedFiles.Add($"{tsFile}.video.m4s");
}
if (audioDownloaded){
@ -1360,6 +1465,7 @@ public class CrunchyrollManager{
Lang = lang.Value,
IsPrimary = isPrimary
});
data.downloadedFiles.Add($"{tsFile}.audio.m4s");
}
}
} else if (options.Novids){
@ -1410,6 +1516,7 @@ public class CrunchyrollManager{
File.WriteAllText($"{tsFile}.txt", string.Join("\r\n", compiledChapters));
files.Add(new DownloadedMedia{ Path = $"{tsFile}.txt", Lang = lang, Type = DownloadMediaType.Chapters });
data.downloadedFiles.Add($"{tsFile}.txt");
} catch{
Console.Error.WriteLine("Failed to write chapter file");
}
@ -1429,9 +1536,9 @@ public class CrunchyrollManager{
} else{
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,
Path = fullPath,
});
data.downloadedFiles.Add(fullPath);
} else{
if (files.All(e => e.Type != DownloadMediaType.Description)){
files.Add(new DownloadedMedia{
Type = DownloadMediaType.Description,
Path = fullPath,
});
data.downloadedFiles.Add(fullPath);
}
}
@ -1649,6 +1758,7 @@ public class CrunchyrollManager{
Lang = sxData.Language,
RelatedVideoDownloadMedia = videoDownloadMedia
});
data.downloadedFiles.Add(sxData.Path);
} else{
Console.WriteLine($"Failed to download subtitle: ${sxData.File}");
}
@ -1678,7 +1788,10 @@ public class CrunchyrollManager{
M3U8Json videoJson = new M3U8Json{
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{
Output = chosenVideoSegments.pssh != null ? $"{tempTsFile}.video.enc.m4s" : $"{tsFile}.video.m4s",
Timeout = options.Timeout,
@ -1689,6 +1802,7 @@ public class CrunchyrollManager{
Override = options.Force,
}, data, true, false);
var videoDownloadResult = await videoDownloader.Download();
return (videoDownloadResult.Ok, videoDownloadResult.Parts, tsFile);
@ -1730,6 +1844,9 @@ public class CrunchyrollManager{
M3U8Json audioJson = new M3U8Json{
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{
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){
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){
CrunchyChapters chapterData = new CrunchyChapters();
@ -1873,7 +1990,7 @@ public class CrunchyrollManager{
JObject jObject = JObject.Parse(showRequestResponse.ResponseContent);
if (jObject.TryGetValue("lastUpdate", out JToken lastUpdateToken)){
chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime?>();
chapterData.lastUpdate = lastUpdateToken.ToObject<DateTime>();
}
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);
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest);
showRequestResponse = await HttpClientReq.Instance.SendHttpRequest(showRequest, true);
if (showRequestResponse.IsOk){
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings);
@ -1988,7 +2105,7 @@ public class CrunchyrollManager{
return true;
}
Console.Error.WriteLine("Old Chapter API request failed");
Console.Error.WriteLine("Chapter request failed");
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(){
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);
CrSeriesSearch? parsedSeries = await crunInstance.CrSeries.ParseSeriesById(seriesId, "ja-JP", true);
if (parsedSeries == null){
Console.Error.WriteLine("Parse Data Invalid - series is maybe only available with VPN or got deleted");
return;
return false;
}
if (parsedSeries.Data != null){
foreach (var s in parsedSeries.Data){
if (!string.IsNullOrEmpty(seasonId) && s.Id != seasonId) continue;
var sId = s.Id;
if (s.Versions is{ Count: > 0 }){
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);
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);
await MatchHistoryEpisodesWithSonarr(false, historySeries);
CfgManager.UpdateHistoryFile();
return true;
}
}
return false;
}
@ -124,10 +128,12 @@ public class History(){
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 downloadDirPath = "";
var videoQuality = "";
List<string> dublist =[];
List<string> sublist =[];
@ -145,6 +151,10 @@ public class History(){
downloadDirPath = historySeries.SeriesDownloadPath;
}
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
videoQuality = historySeries.HistorySeriesVideoQualityOverride;
}
if (historySeason != null){
var historyEpisode = historySeason.EpisodesList.Find(e => e.EpisodeId == episodeId);
if (historySeason.HistorySeasonDubLangOverride.Count > 0){
@ -159,13 +169,17 @@ public class History(){
downloadDirPath = historySeason.SeasonDownloadPath;
}
if (!string.IsNullOrEmpty(historySeason.HistorySeasonVideoQualityOverride)){
videoQuality = historySeason.HistorySeasonVideoQualityOverride;
}
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){
@ -187,10 +201,11 @@ public class History(){
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);
List<string> sublist =[];
var videoQuality = "";
if (historySeries != null){
var historySeason = historySeries.Seasons.FirstOrDefault(s => s.SeasonId == seasonId);
@ -198,12 +213,20 @@ public class History(){
sublist = historySeries.HistorySeriesSoftSubsOverride;
}
if (!string.IsNullOrEmpty(historySeries.HistorySeriesVideoQualityOverride)){
videoQuality = historySeries.HistorySeriesVideoQualityOverride;
}
if (historySeason is{ HistorySeasonSoftSubsOverride.Count: > 0 }){
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);
if (historyEpisode == null){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
@ -260,19 +282,19 @@ public class History(){
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = Languages.SortListByLangList(langList),
HistoryEpisodeAvailableSoftSubs = Languages.SortListByLangList(crunchyEpisode.SubtitleLocales),
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
};
historySeason.EpisodesList.Add(newHistoryEpisode);
} else{
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
//Update existing episode
historyEpisode.EpisodeTitle = GetEpisodeTitle(crunchyEpisode);
historyEpisode.SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _);
@ -366,6 +388,7 @@ public class History(){
historySeries.HistorySeriesAvailableDubLang = Languages.SortListByLangList(series.AudioLocales);
historySeries.HistorySeriesAvailableSoftSubs = Languages.SortListByLangList(series.SubtitleLocales);
}
return;
}
@ -505,16 +528,16 @@ public class History(){
};
foreach (var crunchyEpisode in seasonData){
var langList = new List<string>();
if (crunchyEpisode.Versions != null){
langList.AddRange(crunchyEpisode.Versions.Select(version => version.AudioLocale));
} else{
langList.Add(crunchyEpisode.AudioLocale);
}
Languages.SortListByLangList(langList);
var newHistoryEpisode = new HistoryEpisode{
EpisodeTitle = GetEpisodeTitle(crunchyEpisode),
EpisodeDescription = crunchyEpisode.Description,
@ -524,6 +547,7 @@ public class History(){
SpecialEpisode = !int.TryParse(crunchyEpisode.Episode, out _),
HistoryEpisodeAvailableDubLang = langList,
HistoryEpisodeAvailableSoftSubs = crunchyEpisode.SubtitleLocales,
EpisodeCrPremiumAirDate = crunchyEpisode.PremiumAvailableDate
};
newSeason.EpisodesList.Add(newHistoryEpisode);
@ -531,7 +555,7 @@ public class History(){
return newSeason;
}
public void MatchHistorySeriesWithSonarr(bool updateAll){
if (crunInstance.CrunOptions.SonarrProperties is{ SonarrEnabled: false }){
return;

View file

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

View file

@ -3,10 +3,8 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader.Crunchyroll;
using CRD.Utils;
using CRD.Utils.CustomList;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
@ -102,7 +100,7 @@ public class QueueManager{
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){
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;
Queue.Add(selected);
@ -162,6 +162,12 @@ public class QueueManager{
}
} else{
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));
}
} else{
@ -184,7 +190,7 @@ public class QueueManager{
}
public void CrAddEpMetaToQueue(CrunchyEpMeta epMeta){
public void CrAddMusicMetaToQueue(CrunchyEpMeta epMeta){
Queue.Add(epMeta);
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);
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);

View file

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

View file

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

View file

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

View file

@ -3,13 +3,11 @@ using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using CRD.Downloader;
using System.Runtime.InteropServices;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll;
using Newtonsoft.Json;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.RepresentationModel;
using YamlDotNet.Serialization;
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 PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json");
public static readonly string PathFFMPEG = Path.Combine(WorkingDirectory, "lib", "ffmpeg.exe");
public static readonly string PathMKVMERGE = Path.Combine(WorkingDirectory, "lib", "mkvmerge.exe");
public static readonly string PathMP4Decrypt = Path.Combine(WorkingDirectory, "lib", "mp4decrypt.exe");
private static readonly string ExecutableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
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 PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video");
public static readonly string PathENCODING_PRESETS_DIR = Path.Combine(WorkingDirectory, "presets");
public static readonly string PathTEMP_DIR = Path.Combine(WorkingDirectory, "temp");
public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "video");
public static readonly string PathFONTS_DIR = Path.Combine(WorkingDirectory, "fonts");
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}");
}
}
public static void WriteJsonToFile(string pathToFile, object obj){
try{
// Check if the directory exists; if not, create it.

View file

@ -1,13 +1,11 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Security.Cryptography;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Parser.Utils;
using CRD.Utils.Structs;
using Newtonsoft.Json;
@ -118,11 +116,9 @@ public class HlsDownloader{
}
if (_data.M3U8Json != null){
List<dynamic> segments = _data.M3U8Json.Segments;
// map has init uri outside is none init uri
// Download init part
if (segments[0].map != null && _data.Offset == 0 && !_data.SkipInit){
@ -249,7 +245,7 @@ public class HlsDownloader{
int downloadedSeg = Math.Min(dlOffset, totalSeg);
_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;
// Save resume data to file
@ -268,6 +264,17 @@ public class HlsDownloader{
};
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);
}
@ -285,7 +292,7 @@ public class HlsDownloader{
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
DateTime dateStart = DateTimeOffset.FromUnixTimeMilliseconds(dateStartUnix).UtcDateTime;
double dateElapsed = (DateTime.UtcNow - dateStart).TotalMilliseconds;
@ -293,15 +300,12 @@ public class HlsDownloader{
// Calculate percentage
int percentFixed = (int)((double)partsDownloaded / partsTotal * 100);
int percent = percentFixed < 100 ? percentFixed : (partsTotal == partsDownloaded ? 100 : 99);
// Calculate download speed (bytes per second)
double downloadSpeed = downloadedBytes / (dateElapsed / 1000);
// Calculate remaining time estimate
// double remainingTime = dateElapsed * (partsTotal / (double)partsDownloaded - 1);
int partsLeft = partsTotal - partsDownloaded;
double remainingTime = (partsLeft * (totalDownloadedBytes / partsDownloaded)) / downloadSpeed;
return new Info{
Percent = percent,
Time = remainingTime,
@ -586,32 +590,6 @@ public class Data{
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 int First{ 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;

View file

@ -1,20 +1,23 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices;
using System.Runtime.Serialization;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll;
using CRD.Downloader;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.JsonConv;
using CRD.Utils.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
using Microsoft.Win32;
using Newtonsoft.Json;
namespace CRD.Utils;
@ -28,8 +31,9 @@ public class Helpers{
return JsonConvert.DeserializeObject<T>(json, serializerSettings);
} catch (JsonException ex){
Console.Error.WriteLine($"Error deserializing JSON: {ex.Message}");
throw;
}
return default;
}
public static string ConvertTimeFormat(string time){
@ -67,6 +71,8 @@ public class Helpers{
}
public static void EnsureDirectoriesExist(string path){
Console.WriteLine($"Check if path exists: {path}");
// Check if the path is absolute
bool isAbsolute = Path.IsPathRooted(path);
@ -82,13 +88,17 @@ public class Helpers{
string cumulativePath = isAbsolute ? Path.GetPathRoot(directoryPath) : Environment.CurrentDirectory;
// 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
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++){
// Skip empty parts (which can occur with UNC paths)
// Skip empty parts
if (string.IsNullOrEmpty(directories[i])){
continue;
}
@ -104,6 +114,7 @@ public class Helpers{
}
}
public static bool IsValidPath(string path){
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{
string outputExtension = Path.GetExtension(inputFilePath);
string directory = Path.GetDirectoryName(inputFilePath);
@ -298,18 +318,17 @@ public class Helpers{
string tempOutputFilePath = Path.Combine(directory, $"{fileNameWithoutExtension}_output{outputExtension}");
string additionalParams = string.Join(" ", preset.AdditionalParameters);
string qualityOption;
if (preset.Codec == "h264_nvenc" || preset.Codec == "hevc_nvenc"){
qualityOption = $"-cq {preset.Crf}"; // For NVENC
} else if (preset.Codec == "h264_qsv" || preset.Codec == "hevc_qsv"){
qualityOption = $"-global_quality {preset.Crf}"; // For Intel QSV
} else if (preset.Codec == "h264_amf" || preset.Codec == "hevc_amf"){
qualityOption = $"-qp {preset.Crf}"; // For AMD VCE
string qualityOption = GetQualityOption(preset);
TimeSpan? totalDuration = await GetMediaDurationAsync(CfgManager.PathFFMPEG, inputFilePath);
if (totalDuration == null){
Console.Error.WriteLine("Unable to retrieve input file duration.");
} 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()){
process.StartInfo.FileName = CfgManager.PathFFMPEG;
process.StartInfo.Arguments = ffmpegCommand;
@ -327,6 +346,9 @@ public class Helpers{
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(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){
var vector1 = ComputeWordFrequency(text1);
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{
using (var client = new HttpClient()){
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
var bitmap = new Bitmap(stream);
var response = await HttpClientReq.Instance.GetHttpClient().GetAsync(imageUrl);
if (desiredWidth != 0 && desiredHeight != 0){
var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(desiredWidth, desiredHeight));
if (ChallengeDetector.IsClearanceRequired(response)){
Console.Error.WriteLine($"Cloudflare Challenge detected ");
}
bitmap.Dispose();
return scaledBitmap;
}
return bitmap;
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
var bitmap = new Bitmap(stream);
if (desiredWidth != 0 && desiredHeight != 0){
var scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize(desiredWidth, desiredHeight));
bitmap.Dispose();
return scaledBitmap;
}
return bitmap;
}
} catch (Exception ex){
Console.Error.WriteLine("Failed to load image: " + ex.Message);
@ -513,4 +603,120 @@ public class Helpers{
string uuid = Guid.NewGuid().ToString();
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.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Net.Http.Headers;
using System.Net.Sockets;
using System.Text;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
namespace CRD.Utils;
@ -40,7 +40,6 @@ public class HttpClientReq{
private HttpClientHandler handler;
public HttpClientReq(){
cookieStore = new Dictionary<string, CookieCollection>();
@ -64,7 +63,7 @@ public class HttpClientReq{
Console.Error.WriteLine("No proxy will be used.");
handler = CreateHandler(false);
}
client = new HttpClient(handler);
} else{
Console.Error.WriteLine("No proxy is being used.");
@ -74,10 +73,19 @@ public class HttpClientReq{
Console.Error.WriteLine("No proxy is being used.");
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("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.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(){
@ -132,8 +140,8 @@ public class HttpClientReq{
// handler.CookieContainer.Add(cookie);
// handler.CookieContainer.Add(cookie2);
AddCookie("crunchyroll.com", new Cookie("etp_rt", refreshToken));
AddCookie("crunchyroll.com", new Cookie("c_locale", "en-US"));
AddCookie(".crunchyroll.com", new Cookie("etp_rt", refreshToken));
AddCookie(".crunchyroll.com", new Cookie("c_locale", "en-US"));
}
private void AddCookie(string domain, Cookie cookie){
@ -141,16 +149,26 @@ public class HttpClientReq{
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);
}
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;
try{
AttachCookies(request);
HttpResponseMessage response = await client.SendAsync(request);
if (ChallengeDetector.IsClearanceRequired(response)){
Console.Error.WriteLine($" Cloudflare Challenge detected");
}
content = await response.Content.ReadAsStringAsync();
response.EnsureSuccessStatusCode();
@ -158,28 +176,40 @@ public class HttpClientReq{
return (IsOk: true, ResponseContent: content);
} 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}");
if (!suppressError){
Console.Error.WriteLine($"Error: {e} \n Response: {(content.Length < 500 ? content : "error to long")}");
}
return (IsOk: false, ResponseContent: content);
}
}
private void AttachCookies(HttpRequestMessage request){
if (cookieStore.TryGetValue(request.RequestUri.Host, out CookieCollection cookies)){
var cookieHeader = new StringBuilder();
foreach (Cookie cookie in cookies){
var cookieHeader = new StringBuilder();
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){
cookieHeader.Append("; ");
}
cookieHeader.Append($"{cookie.Name}={cookie.Value}");
}
if (cookieHeader.Length > 0){
request.Headers.Add("Cookie", cookieHeader.ToString());
cookieHeader.Append(cookieString);
}
}
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){
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 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 BetaProfile = ApiBeta + "/accounts/v1/me/profile";
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 CmsN = ApiN + "/content/v2/cms";
public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6";
public static readonly string authBasic = "bm9haWhkZXZtXzZpeWcwYThsMHE6";
public static readonly string authBasicMob = "bm12anNoZmtueW14eGtnN2ZiaDk6WllJVnJCV1VQYmNYRHRiRDIyVlNMYTZiNFdRb3Mzelg=";
public static readonly string authBasicSwitch = "dC1rZGdwMmg4YzNqdWI4Zm4wZnE6eWZMRGZNZnJZdktYaDRKWFMxTEVJMmNDcXUxdjVXYW4=";
public static readonly string authBasicMob = "Basic dXU4aG0wb2g4dHFpOWV0eXl2aGo6SDA2VnVjRnZUaDJ1dEYxM0FBS3lLNE85UTRhX3BlX1o=";
public static readonly string authBasicSwitch = "Basic 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.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CRD.Utils.Structs;
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 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){
var lines = ass.Replace("\r", "").Split('\n');
var styles = new List<string>();
@ -76,7 +124,6 @@ public class FontsManager{
}
public Dictionary<string, List<string>> GetDictFromKeyList(List<string> keysList){
Dictionary<string, List<string>> filteredDictionary = new Dictionary<string, List<string>>();
foreach (string key in keysList){
@ -86,9 +133,8 @@ public class FontsManager{
}
return filteredDictionary;
}
public static string GetFontMimeType(string fontFile){
if (Regex.IsMatch(fontFile, @"\.otf$"))
@ -99,7 +145,7 @@ public class FontsManager{
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>>();
List<string> subsList = new List<string>();
List<ParsedFont> fontsList = new List<ParsedFont>();
@ -108,12 +154,13 @@ public class FontsManager{
foreach (var s in subs){
foreach (var keyValuePair in s.Fonts){
if (!fontsNameList.ContainsKey(keyValuePair.Key)){
fontsNameList.Add(keyValuePair.Key,keyValuePair.Value);
fontsNameList.Add(keyValuePair.Key, keyValuePair.Value);
}
}
subsList.Add(s.Language.Locale);
}
if (subsList.Count > 0){
Console.WriteLine("\nSubtitles: {0} (Total: {1})", string.Join(", ", subsList), subsList.Count);
isNstr = false;

View file

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.Linq;
@ -9,7 +8,6 @@ using System.Threading.Tasks;
using System.Xml;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs;
using DynamicData;
namespace CRD.Utils.Muxing;
@ -37,7 +35,7 @@ public class Merger{
var hasVideo = false;
args.Add("-loglevel warning");
if (!options.mp3){
foreach (var vid in options.OnlyVid){
if (!hasVideo || options.KeepAllVideos == true){
@ -76,6 +74,8 @@ public class Merger{
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 })){
if (sub.value.Delay != null && sub.value.Delay != 0){
double delay = sub.value.Delay / 1000.0 ?? 0;
@ -84,7 +84,9 @@ public class Merger{
args.Add($"-i \"{sub.value.File}\"");
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");
} else{
metaData.Add($"-disposition:s:{sub.i} 0");
@ -146,7 +148,7 @@ public class Merger{
bool hasVideo = false;
args.Add($"-o \"{options.Output}\"");
args.Add($"-o \"{Helpers.AddUncPrefixIfNeeded(options.Output)}\"");
if (options.Options.mkvmerge != null){
args.AddRange(options.Options.mkvmerge);
}
@ -162,11 +164,15 @@ public class Merger{
args.Add($"--language 0:{vid.Language.Code}");
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;
args.Add("--audio-tracks 0");
args.Add("--no-video");
@ -184,11 +190,19 @@ public class Merger{
args.Add($"--sync 0:{aud.Delay}");
}
args.Add($"\"{aud.Path}\"");
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(aud.Path)}\"");
}
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;
if (subObj.Delay.HasValue){
double delay = subObj.Delay ?? 0;
@ -202,7 +216,8 @@ public class Merger{
args.Add($"--track-name {trackName}");
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");
if (CrunchyrollManager.Instance.CrunOptions.DefaultSubForcedDisplay){
args.Add("--forced-track 0:yes");
@ -211,16 +226,16 @@ public class Merger{
} else{
args.Add("--default-track 0:0");
}
if (subObj.ClosedCaption == true && CrunchyrollManager.Instance.CrunOptions.CcSubsMuxingFlag){
args.Add("--hearing-impaired-flag 0:yes");
}
if (subObj.Signs == true && CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced && !isForced){
if (subObj.Signs && CrunchyrollManager.Instance.CrunOptions.SignsSubsAsForced && !isForced){
args.Add("--forced-track 0:yes");
}
args.Add($"\"{subObj.File}\"");
args.Add($"\"{Helpers.AddUncPrefixIfNeeded(subObj.File)}\"");
}
} else{
args.Add("--no-subtitles");
@ -230,14 +245,14 @@ public class Merger{
foreach (var font in options.Fonts){
args.Add($"--attachment-name \"{font.Name}\"");
args.Add($"--attachment-mime-type \"{font.Mime}\"");
args.Add($"--attach-file \"{font.Path}\"");
args.Add($"--attach-file \"{Helpers.AddUncPrefixIfNeeded(font.Path)}\"");
}
} else{
args.Add("--no-attachments");
}
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)){
@ -245,55 +260,105 @@ public class Merger{
}
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);
}
public async Task<double> ProcessVideo(string baseVideoPath, string compareVideoPath){
string baseFramesDir;
string compareFramesDir;
string baseFramesDir, baseFramesDirEnd;
string compareFramesDir, compareFramesDirEnd;
string cleanupDir;
try{
var tempDir = CfgManager.PathTEMP_DIR;
baseFramesDir = Path.Combine(tempDir, "base_frames");
compareFramesDir = Path.Combine(tempDir, "compare_frames");
string uuid = Guid.NewGuid().ToString();
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(baseFramesDirEnd);
Directory.CreateDirectory(compareFramesDir);
Directory.CreateDirectory(compareFramesDirEnd);
} catch (Exception e){
Console.Error.WriteLine(e);
return 0;
}
var extractFramesBase = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 60);
var extractFramesCompare = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 60);
if (!extractFramesBase.IsOk || !extractFramesCompare.IsOk){
Console.Error.WriteLine("Failed to extract Frames to Compare");
return 0;
return -100;
}
var baseFrames = Directory.GetFiles(baseFramesDir).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate)
}).ToList();
try{
var extractFramesBaseStart = await SyncingHelper.ExtractFrames(baseVideoPath, baseFramesDir, 0, 120);
var extractFramesCompareStart = await SyncingHelper.ExtractFrames(compareVideoPath, compareFramesDir, 0, 120);
var compareFrames = Directory.GetFiles(compareFramesDir).Select(fp => new FrameData{
FilePath = fp,
Time = GetTimeFromFileName(fp, extractFramesBase.frameRate)
}).ToList();
TimeSpan? baseVideoDurationTimeSpan = await Helpers.GetMediaDurationAsync(CfgManager.PathFFMPEG, baseVideoPath);
TimeSpan? compareVideoDurationTimeSpan = await Helpers.GetMediaDurationAsync(CfgManager.PathFFMPEG, compareVideoPath);
var offset = SyncingHelper.CalculateOffset(baseFrames, compareFrames);
Console.WriteLine($"Calculated offset: {offset} seconds");
if (baseVideoDurationTimeSpan == null || compareVideoDurationTimeSpan == null){
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);
CleanupDirectory(compareFramesDir);
if (!extractFramesBaseStart.IsOk || !extractFramesCompareStart.IsOk || !extractFramesBaseEnd.IsOk || !extractFramesCompareEnd.IsOk){
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){
@ -365,8 +430,8 @@ public class MergerInput{
public class SubtitleInput{
public LanguageItem Language{ get; set; }
public string File{ get; set; }
public bool? ClosedCaption{ get; set; }
public bool? Signs{ get; set; }
public bool ClosedCaption{ get; set; }
public bool Signs{ get; set; }
public int? Delay{ get; set; }
public DownloadedMedia? RelatedVideoDownloadMedia;

View file

@ -16,7 +16,7 @@ namespace CRD.Utils.Muxing;
public class SyncingHelper{
public static async Task<(bool IsOk, int ErrorCode, double frameRate)> ExtractFrames(string videoPath, string outputDir, double offset, double duration){
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 = "";
@ -37,7 +37,7 @@ public class SyncingHelper{
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine($"{e.Data}");
// Console.WriteLine($"{e.Data}");
output += e.Data;
}
};
@ -128,18 +128,35 @@ public class SyncingHelper{
float[] pixels1 = ExtractPixels(image1, 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
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){
double ssim = ComputeSSIM(imagePath1, imagePath2, 256, 256);
// Console.WriteLine($"SSIM: {ssim}");
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){
var matchingFrame = compareFrames.FirstOrDefault(f => AreFramesSimilar(baseFrame.FilePath, f.FilePath, ssimThreshold));
if (matchingFrame != null){

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
using System.Xml;
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 dynamic locations{ get; set; }

View file

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

View file

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

View file

@ -2,17 +2,14 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Net;
using System.Net.Http;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Sonarr.Models;
using CRD.Views;
using Newtonsoft.Json;
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.Collections.Generic;
using System.ComponentModel;
using System.Net.Http;
using System.Reflection;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
@ -15,19 +11,19 @@ using CRD.Downloader.Crunchyroll;
namespace CRD.Utils.Structs;
public class CalendarWeek{
public DateTime? FirstDayOfWeek{ get; set; }
public DateTime FirstDayOfWeek{ get; set; }
public string? FirstDayOfWeekString{ get; set; }
public List<CalendarDay>? CalendarDays{ get; set; }
}
public class CalendarDay{
public DateTime? DateTime{ get; set; }
public DateTime DateTime{ 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 DateTime? DateTime{ get; set; }
public DateTime DateTime{ get; set; }
public bool? HasPassed{ get; set; }
public string? EpisodeName{ get; set; }
public string? SeriesUrl{ get; set; }
@ -42,32 +38,38 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
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;
[RelayCommand]
public void AddEpisodeToQue(string episodeUrl){
var match = Regex.Match(episodeUrl, "/([^/]+)/watch/([^/]+)");
public void AddEpisodeToQue(){
if (CalendarEpisodes.Count > 0){
foreach (var calendarEpisode in CalendarEpisodes){
calendarEpisode.AddEpisodeToQue();
}
}
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);
if (EpisodeUrl != null){
var match = Regex.Match(EpisodeUrl, "/([^/]+)/watch/([^/]+)");
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{
if (string.IsNullOrEmpty(ThumbnailUrl)){
} else{
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)));
}
}
if (!string.IsNullOrEmpty(ThumbnailUrl)){
ImageBitmap = await Helpers.LoadImage(ThumbnailUrl, width, height);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
} catch (Exception ex){
// Handle exceptions

View file

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

View file

@ -6,15 +6,98 @@ using YamlDotNet.Serialization;
namespace CRD.Utils.Structs;
public class CrDownloadOptions{
#region General Settings
[YamlMember(Alias = "auto_download", ApplyNamingConventions = false)]
public bool AutoDownload{ get; set; }
[YamlMember(Alias = "remove_finished_downloads", ApplyNamingConventions = false)]
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)]
public string Hslang{ get; set; }
@ -45,14 +128,6 @@ public class CrDownloadOptions{
[YamlIgnore]
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)]
public List<string> DlSubs{ get; set; }
@ -62,43 +137,37 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_skip_subs", ApplyNamingConventions = false)]
public bool SkipSubsMux{ get; set; }
[YamlMember(Alias = "subs_add_scaled_border", ApplyNamingConventions = false)]
public ScaledBorderAndShadowSelection SubsAddScaledBorder{ get; set; }
[YamlMember(Alias = "include_signs_subs", ApplyNamingConventions = false)]
public bool IncludeSignsSubs{ get; set; }
[YamlMember(Alias = "mux_signs_subs_flag", ApplyNamingConventions = false)]
public bool SignsSubsAsForced{ get; set; }
[YamlMember(Alias = "include_cc_subs", ApplyNamingConventions = false)]
public bool IncludeCcSubs{ get; set; }
[YamlMember(Alias = "cc_subs_font", ApplyNamingConventions = false)]
public string? CcSubsFont{ get; set; }
[YamlMember(Alias = "mux_cc_subs_flag", ApplyNamingConventions = false)]
public bool CcSubsMuxingFlag{ get; set; }
[YamlMember(Alias = "mux_mp4", ApplyNamingConventions = false)]
public bool Mp4{ get; set; }
[YamlIgnore]
public List<string> Override{ get; set; }
[YamlMember(Alias = "mux_video_title", ApplyNamingConventions = false)]
public string? VideoTitle{ get; set; }
[YamlMember(Alias = "mux_video_description", ApplyNamingConventions = false)]
public bool IncludeVideoDescription{ get; set; }
[YamlMember(Alias = "mux_description_lang", ApplyNamingConventions = false)]
public string? DescriptionLang{ get; set; }
[YamlIgnore]
public string Force{ get; set; }
[YamlMember(Alias = "mux_ffmpeg", ApplyNamingConventions = false)]
public List<string> FfmpegOptions{ get; set; }
@ -107,22 +176,19 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_default_sub", ApplyNamingConventions = false)]
public string DefaultSub{ get; set; }
[YamlMember(Alias = "mux_default_sub_signs", ApplyNamingConventions = false)]
public bool DefaultSubSigns{ get; set; }
[YamlMember(Alias = "mux_default_sub_forced_display", ApplyNamingConventions = false)]
public bool DefaultSubForcedDisplay{ get; set; }
[YamlMember(Alias = "mux_default_dub", ApplyNamingConventions = false)]
public string DefaultAudio{ get; set; }
[YamlIgnore]
public string CcTag{ get; set; }
[YamlMember(Alias = "dl_video_once", ApplyNamingConventions = false)]
public bool DlVideoOnce{ get; set; }
[YamlMember(Alias = "keep_dubs_seperate", ApplyNamingConventions = false)]
public bool KeepDubsSeperate{ get; set; }
@ -131,88 +197,37 @@ public class CrDownloadOptions{
[YamlMember(Alias = "mux_sync_dubs", ApplyNamingConventions = false)]
public bool SyncTiming{ get; set; }
[YamlMember(Alias = "encode_enabled", ApplyNamingConventions = false)]
public bool IsEncodeEnabled{ get; set; }
[YamlMember(Alias = "encode_preset", ApplyNamingConventions = false)]
public string? EncodingPresetName{ get; set; }
[YamlIgnore]
public bool Nocleanup{ get; set; }
[YamlMember(Alias = "chapters", ApplyNamingConventions = false)]
public bool Chapters{ get; set; }
[YamlMember(Alias = "dub_lang", ApplyNamingConventions = false)]
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)]
public string? SelectedCalendarLanguage{ get; set; }
[YamlMember(Alias = "calendar_dub_filter", ApplyNamingConventions = false)]
public string? CalendarDubFilter{ get; set; }
[YamlMember(Alias = "calendar_custom", ApplyNamingConventions = false)]
public bool CustomCalendar{ get; set; }
[YamlMember(Alias = "calendar_hide_dubs", ApplyNamingConventions = false)]
public bool CalendarHideDubs{ get; set; }
[YamlMember(Alias = "calendar_filter_by_air_date", ApplyNamingConventions = false)]
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)]
public string? StreamEndpoint{ 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
}

View file

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

View file

@ -11,5 +11,6 @@ public class CrToken{
public string? country { get; set; }
public string? account_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; }
[JsonProperty("upload_date")]
public DateTime? UploadDate{ get; set; }
public DateTime UploadDate{ get; set; }
[JsonProperty("subtitle_locales")]
public List<Locale>? SubtitleLocales{ get; set; }
[JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; }
public DateTime PremiumAvailableDate{ get; set; }
[JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; }
public DateTime AvailabilityEnds{ get; set; }
[JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; }
public DateTime AvailabilityStarts{ get; set; }
[JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; }
public DateTime FreeAvailableDate{ get; set; }
[JsonProperty("identifier")]
public string? Identifier{ get; set; }
@ -157,10 +157,10 @@ public class CrBrowseEpisodeMetaData{
public string? EligibleRegion{ get; set; }
[JsonProperty("available_date")]
public DateTime? AvailableDate{ get; set; }
public DateTime AvailableDate{ get; set; }
[JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; }
public DateTime PremiumDate{ get; set; }
[JsonProperty("available_offline")]
public bool AvailableOffline{ get; set; }

View file

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

View file

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

View file

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

View file

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
using Newtonsoft.Json;
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.Threading.Tasks;
using CRD.Downloader;
@ -23,6 +24,9 @@ public class HistoryEpisode : INotifyPropertyChanged{
[JsonProperty("episode_cr_season_number")]
public string? EpisodeSeasonNum{ get; set; }
[JsonProperty("episode_cr_premium_air_date")]
public DateTime? EpisodeCrPremiumAirDate{ get; set; }
[JsonProperty("episode_was_downloaded")]
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.Specialized;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls;
using CRD.Downloader;
using Newtonsoft.Json;
namespace CRD.Utils.Structs.History;
@ -31,6 +29,9 @@ public class HistorySeason : INotifyPropertyChanged{
[JsonProperty("series_download_path")]
public string? SeasonDownloadPath{ get; set; }
[JsonProperty("history_season_video_quality_override")]
public string HistorySeasonVideoQualityOverride{ get; set; } = "";
[JsonProperty("history_season_soft_subs_override")]
public List<string> HistorySeasonSoftSubsOverride{ get; set; } =[];
@ -45,7 +46,22 @@ public class HistorySeason : INotifyPropertyChanged{
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]
public string SelectedSubs{ get; set; } = "";
@ -68,7 +84,18 @@ public class HistorySeason : INotifyPropertyChanged{
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(){
HistorySeasonSoftSubsOverride.Clear();
HistorySeasonDubLangOverride.Clear();
@ -88,6 +115,7 @@ public class HistorySeason : INotifyPropertyChanged{
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedSubs)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SelectedDubs)));
@ -99,35 +127,35 @@ public class HistorySeason : INotifyPropertyChanged{
}
public void Init(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
foreach (var languageItem in Languages.languages){
SubLangList.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 dubLang = DubLangList.Where(a => HistorySeasonDubLangOverride.Contains(a.stringValue)).ToList();
SelectedSubLang.Clear();
foreach (var listBoxItem in softSubLang){
SelectedSubLang.Add(listBoxItem);
}
SelectedDubLang.Clear();
foreach (var listBoxItem in dubLang){
SelectedDubLang.Add(listBoxItem);
}
SelectedDubs = string.Join(", ", HistorySeasonDubLangOverride) ?? "";
SelectedSubs = string.Join(", ", HistorySeasonSoftSubsOverride) ?? "";
SelectedSubLang.CollectionChanged += Changes;
SelectedDubLang.CollectionChanged += Changes;
}
#endregion
public void UpdateDownloaded(string? EpisodeId){
@ -151,5 +179,4 @@ public class HistorySeason : INotifyPropertyChanged{
DownloadedEpisodes = EpisodesList.FindAll(e => e.WasDownloaded).Count;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(DownloadedEpisodes)));
}
}
}

View file

@ -1,14 +1,11 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.CustomList;
using Newtonsoft.Json;
@ -52,12 +49,15 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonProperty("history_series_add_date")]
public DateTime? HistorySeriesAddDate{ get; set; }
[JsonProperty("history_series_video_quality_override")]
public string HistorySeriesVideoQualityOverride{ get; set; } = "";
[JsonProperty("history_series_available_soft_subs")]
public List<string> HistorySeriesAvailableSoftSubs{ get; set; } =[];
[JsonProperty("history_series_available_dub_lang")]
public List<string> HistorySeriesAvailableDubLang{ get; set; } =[];
[JsonProperty("history_series_soft_subs_override")]
public List<string> HistorySeriesSoftSubsOverride{ get; set; } =[];
@ -92,7 +92,22 @@ public class HistorySeries : INotifyPropertyChanged{
[JsonIgnore]
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]
public string SelectedSubs{ get; set; } = "";
@ -107,6 +122,27 @@ public class HistorySeries : INotifyPropertyChanged{
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(){
HistorySeriesSoftSubsOverride.Clear();
HistorySeriesDubLangOverride.Clear();
@ -136,16 +172,6 @@ public class HistorySeries : INotifyPropertyChanged{
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(){
if (!(SubLangList.Count > 2 || DubLangList.Count > 0)){
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 dubLang = DubLangList.Where(a => HistorySeriesDubLangOverride.Contains(a.stringValue)).ToList();
@ -181,11 +209,7 @@ public class HistorySeries : INotifyPropertyChanged{
return;
try{
using var client = new HttpClient();
var response = await client.GetAsync(ThumbnailImageUrl);
response.EnsureSuccessStatusCode();
using var stream = await response.Content.ReadAsStreamAsync();
ThumbnailImage = new Bitmap(stream);
ThumbnailImage = await Helpers.LoadImage(ThumbnailImageUrl);
IsImageLoaded = true;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ThumbnailImage)));
@ -320,10 +344,15 @@ public class HistorySeries : INotifyPropertyChanged{
Console.WriteLine($"Fetching Data for: {SeriesTitle}");
FetchingData = true;
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(SeriesDescription)));
// CrunchyrollManager.Instance.History.MatchHistoryEpisodesWithSonarr(false, this);
UpdateNewEpisodes();
FetchingData = false;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(FetchingData)));

View file

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using CommunityToolkit.Mvvm.ComponentModel;
using CRD.Views;
using Newtonsoft.Json;
@ -130,4 +131,14 @@ public class NavigationMessage{
Back = back;
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.Globalization;
using Avalonia.Data.Converters;
using FluentAvalonia.UI.Controls;
namespace CRD.Utils.UI;

View file

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

View file

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

View file

@ -1,15 +1,20 @@
using System;
using System.Globalization;
using Avalonia;
using Avalonia.Data.Converters;
using Avalonia.Media;
using FluentAvalonia.UI.Controls;
using Avalonia.Styling;
namespace CRD.Utils.UI;
public class UiValueConverterCalendarBackground : IValueConverter{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture){
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"));

View file

@ -1,24 +1,25 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Newtonsoft.Json;
namespace CRD.Utils.Updater;
public class Updater : INotifyPropertyChanged{
public double progress = 0;
#region Singelton
private static Updater? _instance;
private static readonly object Padlock = new();
public static Updater Instance{
get{
if (_instance == null){
@ -49,20 +50,53 @@ public class Updater : INotifyPropertyChanged{
public async Task<bool> CheckForUpdatesAsync(){
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();
handler.UseProxy = false;
using (var client = new HttpClient(handler)){
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
var response = await client.GetStringAsync(apiEndpoint);
var releaseInfo = Helpers.Deserialize<dynamic>(response,null);
var releaseInfo = Helpers.Deserialize<dynamic>(response, null);
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 currentVersion = $"v{version?.Major}.{version?.Minor}.{version?.Build}";
if (latestVersion != currentVersion){
Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion);
return true;
@ -80,43 +114,45 @@ public class Updater : INotifyPropertyChanged{
public async Task DownloadAndUpdateAsync(){
try{
using (var client = new HttpClient()){
// Download the zip file
var response = await client.GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
// Download the zip file
var response = await HttpClientReq.Instance.GetHttpClient().GetAsync(downloadUrl, HttpCompletionOption.ResponseHeadersRead);
if (response.IsSuccessStatusCode){
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
var totalBytesRead = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
if (response.IsSuccessStatusCode){
var totalBytes = response.Content.Headers.ContentLength ?? -1L;
var totalBytesRead = 0L;
var buffer = new byte[8192];
var isMoreToRead = true;
using (var stream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)){
do{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0){
isMoreToRead = false;
progress = 100;
OnPropertyChanged(nameof(progress));
continue;
}
using (var stream = await response.Content.ReadAsStreamAsync())
using (var fileStream = new FileStream(tempPath, FileMode.Create, FileAccess.Write, FileShare.None)){
do{
var bytesRead = await stream.ReadAsync(buffer, 0, buffer.Length);
if (bytesRead == 0){
isMoreToRead = false;
progress = 100;
OnPropertyChanged(nameof(progress));
continue;
}
await fileStream.WriteAsync(buffer, 0, bytesRead);
await fileStream.WriteAsync(buffer, 0, bytesRead);
totalBytesRead += bytesRead;
if (totalBytes != -1){
progress = (double)totalBytesRead / totalBytes * 100;
OnPropertyChanged(nameof(progress));
}
} while (isMoreToRead);
}
ZipFile.ExtractToDirectory(tempPath, extractPath, true);
ApplyUpdate(extractPath);
} else{
Console.Error.WriteLine("Failed to get Update");
totalBytesRead += bytesRead;
if (totalBytes != -1){
progress = (double)totalBytesRead / totalBytes * 100;
OnPropertyChanged(nameof(progress));
}
} while (isMoreToRead);
}
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){
Console.Error.WriteLine($"Failed to get Update: {e.Message}");

View file

@ -1,14 +1,12 @@
using System;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using CRD.Downloader;
using CRD.Downloader.Crunchyroll;
using CRD.Utils.Structs;
using CRD.Utils;
using CRD.Views.Utils;
using FluentAvalonia.UI.Controls;
using Newtonsoft.Json;
@ -61,9 +59,10 @@ public partial class AccountPageViewModel : ViewModelBase{
}
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
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;
@ -139,13 +138,7 @@ public partial class AccountPageViewModel : ViewModelBase{
public async void LoadProfileImage(string imageUrl){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ProfileImage = new Bitmap(stream);
}
}
ProfileImage = await Helpers.LoadImage(imageUrl);
} catch (Exception ex){
// Handle exceptions
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.Structs;
using CRD.Utils.Structs.Crunchyroll.Music;
// ReSharper disable InconsistentNaming
namespace CRD.ViewModels;
@ -56,7 +57,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
public ObservableCollection<ItemModel> Items{ 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]
public CrBrowseSeries _selectedSearchItem;
@ -64,7 +65,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty]
public ComboBoxItem _currentSelectedSeason;
public ObservableCollection<ComboBoxItem> SeasonList{ get;set; } = new();
public ObservableCollection<ComboBoxItem> SeasonList{ get; set; } = new();
private Dictionary<string, List<ItemModel>> episodesBySeason = new();
@ -75,7 +76,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private CrunchyMusicVideoList? currentMusicVideoList;
private bool CurrentSeasonFullySelected = false;
public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
}
@ -200,7 +201,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (music != null){
var meta = musicClass.EpisodeMeta(music);
QueueManager.Instance.CrAddEpMetaToQueue(meta);
QueueManager.Instance.CrAddMusicMetaToQueue(meta);
}
}
}
@ -243,11 +244,10 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
ButtonEnabled = false;
SearchVisible = true;
SlectSeasonVisible = false;
//TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
}
private async Task HandleUrlInputAsync(){
@ -426,7 +426,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
partial void OnCurrentSelectedSeasonChanging(ComboBoxItem? oldValue, ComboBoxItem newValue){
if(SelectedItems == null) return;
if (SelectedItems == null) return;
foreach (var selectedItem in SelectedItems){
if (!selectedEpisodes.Contains(selectedItem.AbsolutNum)){
selectedEpisodes.Add(selectedItem.AbsolutNum);
@ -443,8 +443,8 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
private void OnSelectedItemsChanged(object? sender, NotifyCollectionChangedEventArgs e){
if(Items == null) return;
if (Items == null) return;
CurrentSeasonFullySelected = Items.All(item => SelectedItems.Contains(item));
if (CurrentSeasonFullySelected){
@ -523,7 +523,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
SelectedItems.Clear();
episodesBySeason.Clear();
SeasonList.Clear();
//TODO - find a better way to reduce ram usage
GCSettings.LargeObjectHeapCompactionMode = GCLargeObjectHeapCompactionMode.CompactOnce;
GC.Collect();
@ -597,28 +597,25 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
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){
itemModel.ImageBitmap?.Dispose(); // Dispose the bitmap if it exists
itemModel.ImageBitmap = null; // Nullify the reference to avoid lingering references
}
// Clear collections and other managed resources
Items.Clear();
Items = null;
SearchItems.Clear();
SearchItems = null;
SelectedItems.Clear();
SelectedItems = null;
SeasonList.Clear();
SeasonList = null;
episodesBySeason.Clear();
episodesBySeason = null;
selectedEpisodes.Clear();
selectedEpisodes = null;
// Clear collections and other managed resources
Items.Clear();
Items = null;
SearchItems.Clear();
SearchItems = null;
SelectedItems.Clear();
SelectedItems = null;
SeasonList.Clear();
SeasonList = null;
episodesBySeason.Clear();
episodesBySeason = null;
selectedEpisodes.Clear();
selectedEpisodes = null;
}
}
public class ItemModel(string id, string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
@ -640,7 +637,7 @@ public class ItemModel(string id, string imageUrl, string description, string ti
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url,208,117);
ImageBitmap = await Helpers.LoadImage(url, 208, 117);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}

View file

@ -1,27 +1,33 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
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 DynamicData;
using DynamicData.Kernel;
using Newtonsoft.Json;
namespace CRD.ViewModels;
public partial class CalendarPageViewModel : ViewModelBase{
public ObservableCollection<CalendarDay> CalendarDays{ get; set; }
[ObservableProperty]
private bool _prevButtonEnabled = true;
[ObservableProperty]
private bool _nextButtonEnabled = true;
[ObservableProperty]
private bool _showLoading;
@ -62,9 +68,8 @@ public partial class CalendarPageViewModel : ViewModelBase{
private CalendarWeek? currentWeek;
private bool loading = true;
public CalendarPageViewModel(){
public CalendarPageViewModel(){
CalendarDays = new ObservableCollection<CalendarDay>();
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];
loading = false;
LoadCalendar(GetThisWeeksMondayDate(), false);
LoadCalendar(GetThisWeeksMondayDate(), DateTime.Now, false);
}
private string GetThisWeeksMondayDate(){
// Get today's date
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;
// 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){
daysToSubtract += 7;
}
// Get the date of the most recent Monday
DateTime monday = today.AddDays(-daysToSubtract);
// Format and print the date
string formattedDate = monday.ToString("yyyy-MM-dd");
return formattedDate;
}
public async void LoadCalendar(string mondayDate, bool forceUpdate){
public async void LoadCalendar(string mondayDate,DateTime customCalDate, bool forceUpdate){
ShowLoading = true;
CalendarWeek week;
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{
PrevButtonEnabled = true;
NextButtonEnabled = true;
week = await CalendarManager.Instance.GetCalendarForDate(mondayDate, forceUpdate);
if (currentWeek != null && currentWeek == week){
ShowLoading = false;
@ -129,7 +141,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
foreach (var calendarDay in CalendarDays){
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
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){
calendarDayCalendarEpisode.LoadImage();
if (calendarDayCalendarEpisode.AnilistEpisode){
calendarDayCalendarEpisode.LoadImage(100,150);
} else{
calendarDayCalendarEpisode.LoadImage();
}
}
}
}
@ -175,7 +196,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
mondayDate = GetThisWeeksMondayDate();
}
LoadCalendar(mondayDate, true);
var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(6);
}
LoadCalendar(mondayDate,refreshDate, true);
}
[RelayCommand]
@ -186,13 +212,18 @@ public partial class CalendarPageViewModel : ViewModelBase{
string mondayDate;
if (currentWeek is{ FirstDayOfWeek: not null }){
mondayDate = PreviousMonday((DateTime)currentWeek.FirstDayOfWeek);
if (currentWeek is{ FirstDayOfWeek: var firstDay } && firstDay != DateTime.MinValue){
mondayDate = PreviousMonday(currentWeek.FirstDayOfWeek);
} else{
mondayDate = GetThisWeeksMondayDate();
}
var refreshDate = DateTime.Now;
if (currentWeek?.FirstDayOfWeek != null){
refreshDate = currentWeek.FirstDayOfWeek.AddDays(-1);
}
LoadCalendar(mondayDate, false);
LoadCalendar(mondayDate,refreshDate, false);
}
[RelayCommand]
@ -203,13 +234,20 @@ public partial class CalendarPageViewModel : ViewModelBase{
string mondayDate;
if (currentWeek is{ FirstDayOfWeek: not null }){
mondayDate = NextMonday((DateTime)currentWeek.FirstDayOfWeek);
if (currentWeek is{ FirstDayOfWeek: var firstDay } && firstDay != DateTime.MinValue){
mondayDate = NextMonday(currentWeek.FirstDayOfWeek);
} else{
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;
LoadCalendar(GetThisWeeksMondayDate(), true);
LoadCalendar(GetThisWeeksMondayDate(),DateTime.Now, true);
CfgManager.WriteSettingsToFile();
}
@ -265,4 +303,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteSettingsToFile();
}
}
}

View file

@ -1,8 +1,8 @@
using System;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using CommunityToolkit.Mvvm.ComponentModel;
@ -59,7 +59,6 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
public string DoingWhat{ get; set; }
public string DownloadSpeed{ get; set; }
public string InfoText{ 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");
DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s";
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;
}
@ -94,9 +95,9 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
}
private string GetSubtitleString(){
var hardSubs = CrunchyrollManager.Instance.CrunOptions.Hslang != "none" ? "Hardsub: " : "";
var hardSubs = epMeta.Hslang != "none" ? "Hardsub: " : "";
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)){
hardSubs += locale + " ";
}
@ -133,9 +134,11 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
DownloadSpeed = $"{epMeta.DownloadProgress.DownloadSpeed / 1000000.0:F2}Mb/s";
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;
@ -192,6 +195,16 @@ public partial class DownloadItemModel : INotifyPropertyChanged{
CrunchyEpMeta? downloadItem = QueueManager.Instance.Queue.FirstOrDefault(e => e.Equals(epMeta)) ?? null;
if (downloadItem != null){
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.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Runtime.Serialization;
using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls;
@ -19,9 +16,7 @@ using CRD.Utils;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using CRD.Views;
using DynamicData;
using HarfBuzzSharp;
using ReactiveUI;
namespace CRD.ViewModels;
@ -29,10 +24,10 @@ namespace CRD.ViewModels;
public partial class HistoryPageViewModel : ViewModelBase{
public ObservableCollection<HistorySeries> Items{ get; }
public ObservableCollection<HistorySeries> FilteredItems{ get; }
[ObservableProperty]
private ProgramManager _programManager;
[ObservableProperty]
private HistorySeries _selectedSeries;
@ -107,14 +102,15 @@ public partial class HistoryPageViewModel : ViewModelBase{
[ObservableProperty]
private static bool _sonarrAvailable;
[ObservableProperty]
private static string _progressText;
public HistoryPageViewModel(){
ProgramManager = ProgramManager.Instance;
_storageProvider = ProgramManager.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.StorageProvider));
if (CrunchyrollManager.Instance.CrunOptions.SonarrProperties != null){
SonarrAvailable = CrunchyrollManager.Instance.CrunOptions.SonarrProperties.SonarrEnabled;
} else{
@ -149,11 +145,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
}
foreach (FilterType filterType in Enum.GetValues(typeof(FilterType))){
if (!SonarrAvailable && (filterType == FilterType.MissingEpisodesSonarr || filterType == FilterType.ContinuingOnly)){
continue;
}
var item = new FilterListElement(){ FilterTitle = filterType.GetEnumMemberValue(), SelectedType = filterType };
FilterList.Add(item);
if (filterType == currentFilterType){
@ -224,7 +219,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
if (SelectedFilter != null){
OnSelectedFilterChanged(SelectedFilter);
}
} else{
Console.Error.WriteLine("Invalid viewtype selected");
}
@ -235,11 +229,10 @@ public partial class HistoryPageViewModel : ViewModelBase{
partial void OnSelectedFilterChanged(FilterListElement? value){
if (value == null){
return;
}
currentFilterType = value.SelectedType;
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;
NavToSeries();
@ -399,17 +396,17 @@ public partial class HistoryPageViewModel : ViewModelBase{
Console.Error.WriteLine($"[Sonarr Match] {series.Title} already matched");
}
});
var seriesIds = concurrentSeriesIds.ToList();
var totalSeries = seriesIds.Count;
for (int count = 0; count < totalSeries; count++){
ProgressText = $"{count + 1}/{totalSeries}";
// Await the CRUpdateSeries task for each seriesId
await crInstance.History.CRUpdateSeries(seriesIds[count], "");
}
// var updateTasks = seriesIds.Select(seriesId => crInstance.History.CRUpdateSeries(seriesId, ""));
// await Task.WhenAll(updateTasks);
}
@ -495,11 +492,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
await Task.WhenAll(downloadTasks);
}
public void SetStorageProvider(IStorageProvider storageProvider){
_storageProvider = storageProvider ?? throw new ArgumentNullException(nameof(storageProvider));
}
}
public class HistoryPageProperties(){

View file

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

View file

@ -1,904 +1,54 @@
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.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.Crunchyroll;
using CRD.Utils;
using CRD.Utils.Ffmpeg_Encoding;
using CRD.Utils.Sonarr;
using CRD.Utils.Structs;
using CRD.Utils.Structs.History;
using Avalonia.Layout;
using Avalonia.Media.Imaging;
using CRD.Downloader.Crunchyroll.ViewModels;
using CRD.Downloader.Crunchyroll.Views;
using CRD.ViewModels.Utils;
using CRD.Views.Utils;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls;
using Image = Avalonia.Controls.Image;
// ReSharper disable InconsistentNaming
namespace CRD.ViewModels;
public partial class SettingsPageViewModel : ViewModelBase{
[ObservableProperty]
private string _currentVersion;
[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 bool _downloadToTempFolder;
[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 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 ObservableCollection<TabViewItem> Tabs{ get; } = new();
private TabViewItem CreateTab(string header, string iconPath, UserControl content, object viewModel){
content.DataContext = viewModel;
Bitmap bitmap = null;
try{
// Load the image using AssetLoader.Open
bitmap = new Bitmap(Avalonia.Platform.AssetLoader.Open(new Uri(iconPath)));
} catch (Exception ex){
Console.WriteLine($"Error loading image: {ex.Message}");
}
return new TabViewItem{
Header = new StackPanel{
Orientation = Orientation.Horizontal,
Spacing = 5,
Children ={
new Image{ Source = bitmap, Width = 18, Height = 18 },
new TextBlock{ Text = header, FontSize = 16}
}
},
IsClosable = false,
Content = content
};
}
public SettingsPageViewModel(){
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;
}
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;
// Add initial tabs
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()));
}
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.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
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;
namespace CRD.ViewModels;

View file

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

View file

@ -75,6 +75,10 @@
Width="120"
Height="180" />
</Grid>
@ -91,6 +95,14 @@
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
Text="{Binding Description}">
</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>

View file

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

View file

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

View file

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

View file

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

View file

@ -38,9 +38,9 @@
VerticalAlignment="Center"
Command="{Binding RefreshAll}"
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
<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>
</Button>
@ -50,7 +50,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<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>
</Button>
@ -60,7 +60,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<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>
</ToggleButton>
@ -74,7 +74,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:ImageIcon VerticalAlignment="Center" HorizontalAlignment="Center" Margin="0 1 0 0" Source="../Assets/sonarr.png" Width="30" Height="30" />
<TextBlock Text="Sonarr" TextWrapping="Wrap" FontSize="12"></TextBlock>
<TextBlock Text="Sonarr" TextWrapping="Wrap" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
@ -123,7 +123,7 @@
IsChecked="{Binding ViewSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="View" FontSize="32" />
<TextBlock Text="View" FontSize="12"></TextBlock>
<TextBlock Text="View" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
@ -147,7 +147,7 @@
IsChecked="{Binding SortingSelectionOpen}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Sort" FontSize="32" />
<TextBlock Text="Sort" FontSize="12"></TextBlock>
<TextBlock Text="Sort" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
@ -178,7 +178,7 @@
IsEnabled="{Binding !ProgramManager.FetchingData}">
<StackPanel Orientation="Vertical">
<controls:SymbolIcon Symbol="Filter" FontSize="32" />
<TextBlock Text="Filter" FontSize="12"></TextBlock>
<TextBlock Text="Filter" HorizontalAlignment="Center" FontSize="12"></TextBlock>
</StackPanel>
</ToggleButton>
<Popup IsLightDismissEnabled="True"
@ -214,80 +214,82 @@
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<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>
<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>
<Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 0 10"
Background="#f78c25" Opacity="1"
Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} "
IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}">
<TextBlock VerticalAlignment="Center" TextAlignment="Center" Width="30"
Height="30"
Text="{Binding NewEpisodes}"
Padding="0,5,0,0" />
</Border>
<Border VerticalAlignment="Top" HorizontalAlignment="Right" CornerRadius="0 0 0 10"
Background="#f78c25" Opacity="1"
Margin="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).CornerMargin} "
IsVisible="{Binding NewEpisodes, Converter={StaticResource UiIntToVisibilityConverter}}">
<TextBlock VerticalAlignment="Center" TextAlignment="Center" Width="30"
Height="30"
Text="{Binding NewEpisodes}"
Padding="0,5,0,0" />
</Border>
</Grid>
<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>
<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>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding FetchingData}">
<controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
</Grid>
<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>
</Border>
<Grid HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="#90000000" IsVisible="{Binding FetchingData}">
<controls:ProgressRing Width="100" Height="100"></controls:ProgressRing>
</Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
@ -422,7 +424,7 @@
VerticalAlignment="Center"
IsVisible="{Binding EditModeEnabled}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
<controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel>
</ToggleButton>
@ -435,11 +437,51 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer>
<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>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -476,7 +518,7 @@
</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>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
@ -724,7 +766,7 @@
VerticalAlignment="Center"
IsVisible="{Binding $parent[ScrollViewer].((history:HistorySeries)DataContext).EditModeEnabled}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
<controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel>
</ToggleButton>
@ -737,11 +779,52 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer>
<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>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -778,7 +861,7 @@
</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>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">

View file

@ -10,10 +10,11 @@
x:DataType="vm:MainWindowViewModel"
Icon="/Assets/app_icon.ico"
Title="Crunchy-Downloader">
<Design.DataContext>
<vm:MainWindowViewModel />
</Design.DataContext>
<Grid Name="MainGrid">
<ContentControl x:Name="MainContent">
<Grid RowDefinitions="Auto, *">
@ -60,6 +61,8 @@
IconSource="Add" />
<ui:NavigationViewItem Classes="SampleAppNav" Content="Calendar" Tag="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"
IconSource="Library" />
</ui:NavigationView.MenuItems>

View file

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

View file

@ -55,7 +55,7 @@
<RowDefinition Height="Auto" />
</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="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>
@ -121,7 +121,7 @@
VerticalAlignment="Center"
IsVisible="{Binding EditMode}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
<controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel>
</ToggleButton>
@ -134,11 +134,56 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer>
<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>
<StackPanel>
<ToggleButton x:Name="DropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -175,7 +220,7 @@
</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>
<StackPanel>
<ToggleButton x:Name="DropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">
@ -426,7 +471,7 @@
VerticalAlignment="Center"
IsVisible="{Binding $parent[UserControl].((vm:SeriesPageViewModel)DataContext).EditMode}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="Speaker2" FontSize="18" />
<controls:SymbolIcon Symbol="Settings" FontSize="18" />
</StackPanel>
</ToggleButton>
@ -439,11 +484,52 @@
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<StackPanel>
<controls:SettingsExpander Header="Language Override">
<controls:SettingsExpander Header="Settings Override">
<controls:SettingsExpander.Footer>
<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>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonDub" Width="210" HorizontalContentAlignment="Stretch">
@ -480,7 +566,7 @@
</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>
<StackPanel>
<ToggleButton x:Name="SeasonOverrideDropdownButtonSub" Width="210" HorizontalContentAlignment="Stretch">

View file

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

View file

@ -4,7 +4,6 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="vm:SettingsPageViewModel"
x:Class="CRD.Views.SettingsPageView"
@ -14,894 +13,10 @@
<vm:SettingsPageViewModel />
</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>
<!-- <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>
<controls:TabView TabItems="{Binding Tabs}"
AllowDropTabs="False" IsAddTabButtonVisible="False"
Background="Transparent" CanDragTabs="False" CanReorderTabs="False"
VerticalAlignment="Stretch">
</controls:TabView>
</UserControl>

View file

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

View file

@ -1,5 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
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.Markup.Xaml;
using Avalonia.Controls;
namespace CRD.Views.Utils;

View file

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

View file

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

View file

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Controls;
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}" />
</application>
</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>