Add - Add search functionality to the add tab

Add - Add custom calendar option to display newly released episodes for the last 6 days and the current day
Fix - Fix decryption issue with file paths containing special characters
This commit is contained in:
Elwador 2024-07-12 04:35:33 +02:00
parent 4e98b64527
commit acdbc7467b
16 changed files with 1049 additions and 222 deletions

View File

@ -138,7 +138,7 @@ public class CrAuth{
}
}
public async void LoginWithToken(){
public async Task LoginWithToken(){
if (crunInstance.Token?.refresh_token == null){
Console.Error.WriteLine("Missing Refresh Token");
return;

View File

@ -160,7 +160,6 @@ public class CrEpisode(){
var retMeta = new CrunchyEpMeta();
for (int index = 0; index < episodeP.EpisodeAndLanguages.Items.Count; index++){
var item = episodeP.EpisodeAndLanguages.Items[index];
@ -186,8 +185,10 @@ public class CrEpisode(){
var epMeta = new CrunchyEpMeta();
epMeta.Data = new List<CrunchyEpMetaData>{ new(){ MediaId = item.Id, Versions = item.Versions, IsSubbed = item.IsSubbed, IsDubbed = item.IsDubbed } };
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ?? Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeriesTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeriesTitle)).SeriesTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeriesTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.SeasonTitle = episodeP.EpisodeAndLanguages.Items.FirstOrDefault(a => !dubPattern.IsMatch(a.SeasonTitle)).SeasonTitle ??
Regex.Replace(episodeP.EpisodeAndLanguages.Items[0].SeasonTitle, @"\(\w+ Dub\)", "").TrimEnd();
epMeta.EpisodeNumber = item.Episode;
epMeta.EpisodeTitle = item.Title;
epMeta.SeasonId = item.SeasonId;
@ -223,7 +224,6 @@ public class CrEpisode(){
if (retMeta.Data != null){
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
retMeta.Data.Add(epMetaData);
} else{
epMetaData.Lang = episodeP.EpisodeAndLanguages.Langs[index];
epMeta.Data[0] = epMetaData;
@ -238,4 +238,31 @@ public class CrEpisode(){
return retMeta;
}
public async Task<CrBrowseEpisodeBase?> GetNewEpisodes(string? crLocale){
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
}
query["n"] = "200";
query["sort_by"] = "newly_added";
query["type"] = "episode";
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine("Series Request Failed");
return null;
}
CrBrowseEpisodeBase? series = Helpers.Deserialize<CrBrowseEpisodeBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
return series;
}
}

View File

@ -11,13 +11,13 @@ using System.Web;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Views;
using DynamicData;
using Newtonsoft.Json;
using ReactiveUI;
namespace CRD.Downloader;
public class CrSeries(){
private readonly Crunchyroll crunInstance = Crunchyroll.Instance;
public async Task<List<CrunchyEpMeta>> DownloadFromSeriesId(string id, CrunchyMultiDownload data){
@ -156,7 +156,6 @@ public class CrSeries(){
int fallbackIndex = 0;
var seasonData = await GetSeasonDataById(s.Id, "");
if (seasonData.Data != null){
if (crunInstance.CrunOptions.History){
crunInstance.CrHistory.UpdateWithSeasonData(seasonData);
}
@ -288,14 +287,8 @@ public class CrSeries(){
public async Task<CrunchyEpisodeList> GetSeasonDataById(string seasonID, string? crLocale, bool forcedLang = false, bool log = false){
CrunchyEpisodeList episodeList = new CrunchyEpisodeList(){ Data = new List<CrunchyEpisode>(), Total = 0, Meta = new Meta() };
if (crunInstance.CmsToken?.Cms == null){
Console.Error.WriteLine("Missing CMS Token");
return episodeList;
}
NameValueCollection query;
if (log){
query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP";
@ -377,11 +370,6 @@ public class CrSeries(){
}
public async Task<CrSeriesSearch?> ParseSeriesById(string id, string? crLocale, bool forced = false){
if (crunInstance.CmsToken?.Cms == null){
Console.Error.WriteLine("Missing CMS Access Token");
return null;
}
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP";
@ -390,7 +378,6 @@ public class CrSeries(){
if (forced){
query["force_locale"] = crLocale;
}
}
@ -414,11 +401,6 @@ public class CrSeries(){
}
public async Task<CrSeriesBase?> SeriesById(string id, string? crLocale, bool forced = false){
if (crunInstance.CmsToken?.Cms == null){
Console.Error.WriteLine("Missing CMS Access Token");
return null;
}
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
query["preferred_audio_language"] = "ja-JP";
@ -427,7 +409,6 @@ public class CrSeries(){
if (forced){
query["force_locale"] = crLocale;
}
}
var request = HttpClientReq.CreateRequestMessage($"{Api.Cms}/series/{id}", HttpMethod.Get, true, true, query);
@ -449,4 +430,71 @@ public class CrSeries(){
return series;
}
public async Task<CrSearchSeriesBase?> Search(string searchString,string? crLocale){
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
}
query["q"] = searchString;
query["n"] = "6";
query["type"] = "top_results";
var request = HttpClientReq.CreateRequestMessage($"{Api.Search}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine("Series Request Failed");
return null;
}
CrSearchSeriesBase? series = Helpers.Deserialize<CrSearchSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
return series;
}
public async Task<CrBrowseSeriesBase?> GetAllSeries(string? crLocale){
CrBrowseSeriesBase? complete = new CrBrowseSeriesBase();
complete.Data =[];
var i = 0;
do{
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
if (!string.IsNullOrEmpty(crLocale)){
query["locale"] = crLocale;
}
query["start"] = i + "";
query["n"] = "50";
query["sort_by"] = "alphabetical";
var request = HttpClientReq.CreateRequestMessage($"{Api.Browse}", HttpMethod.Get, true, false, query);
var response = await HttpClientReq.Instance.SendHttpRequest(request);
if (!response.IsOk){
Console.Error.WriteLine("Series Request Failed");
return null;
}
CrBrowseSeriesBase? series = Helpers.Deserialize<CrBrowseSeriesBase>(response.ResponseContent, crunInstance.SettingsJsonSerializerSettings);
if (series != null){
complete.Total = series.Total;
if (series.Data != null) complete.Data.AddRange(series.Data);
} else{
break;
}
i += 50;
} while (i < complete.Total);
return complete;
}
}

View File

@ -51,7 +51,19 @@ public class Crunchyroll{
#region Calendar Variables
private Dictionary<string, CalendarWeek> calendar = new();
private Dictionary<string, string> calendarLanguage = new();
private Dictionary<string, string> calendarLanguage = new(){
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
{ "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" },
{ "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" },
{ "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" },
{ "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" },
{ "de", "https://www.crunchyroll.com/de/simulcastcalendar" },
{ "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" },
{ "it", "https://www.crunchyroll.com/it/simulcastcalendar" },
{ "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" },
{ "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" },
};
#endregion
@ -123,14 +135,6 @@ public class Crunchyroll{
HasPremium = false,
};
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
CrAuth.LoginWithToken();
} else{
await CrAuth.AuthAnonymous();
}
Console.WriteLine($"Can Decrypt: {_widevine.canDecrypt}");
CrunOptions.AutoDownload = false;
@ -164,6 +168,19 @@ public class Crunchyroll{
CfgManager.UpdateSettingsFromFile();
if (CrunOptions.LogMode){
CfgManager.EnableLogMode();
} else{
CfgManager.DisableLogMode();
}
if (CfgManager.CheckIfFileExists(CfgManager.PathCrToken)){
Token = CfgManager.DeserializeFromFile<CrToken>(CfgManager.PathCrToken);
CrAuth.LoginWithToken();
} else{
await CrAuth.AuthAnonymous();
}
if (CrunOptions.History){
if (File.Exists(CfgManager.PathCrHistory)){
HistoryList = JsonConvert.DeserializeObject<ObservableCollection<HistorySeries>>(File.ReadAllText(CfgManager.PathCrHistory)) ??[];
@ -172,25 +189,8 @@ public class Crunchyroll{
RefreshSonarr();
}
if (CrunOptions.LogMode){
CfgManager.EnableLogMode();
} else{
CfgManager.DisableLogMode();
}
calendarLanguage = new(){
{ "en-us", "https://www.crunchyroll.com/simulcastcalendar" },
{ "es", "https://www.crunchyroll.com/es/simulcastcalendar" },
{ "es-es", "https://www.crunchyroll.com/es-es/simulcastcalendar" },
{ "pt-br", "https://www.crunchyroll.com/pt-br/simulcastcalendar" },
{ "pt-pt", "https://www.crunchyroll.com/pt-pt/simulcastcalendar" },
{ "fr", "https://www.crunchyroll.com/fr/simulcastcalendar" },
{ "de", "https://www.crunchyroll.com/de/simulcastcalendar" },
{ "ar", "https://www.crunchyroll.com/ar/simulcastcalendar" },
{ "it", "https://www.crunchyroll.com/it/simulcastcalendar" },
{ "ru", "https://www.crunchyroll.com/ru/simulcastcalendar" },
{ "hi", "https://www.crunchyroll.com/hi/simulcastcalendar" },
};
}
public async void RefreshSonarr(){
@ -300,7 +300,7 @@ public class Crunchyroll{
calEpisode.DateTime = episodeTime;
calEpisode.HasPassed = hasPassed;
calEpisode.EpisodeName = episodeName;
calEpisode.SeasonUrl = seasonLink;
calEpisode.SeriesUrl = seasonLink;
calEpisode.EpisodeUrl = episodeLink;
calEpisode.ThumbnailUrl = thumbnailUrl;
calEpisode.IsPremiumOnly = isPremiumOnly;
@ -1194,8 +1194,10 @@ public class Crunchyroll{
var keyId = BitConverter.ToString(encryptionKeys[0].KeyID).Replace("-", "").ToLower();
var key = BitConverter.ToString(encryptionKeys[0].Bytes).Replace("-", "").ToLower();
var commandBase = $"--show-progress --key {keyId}:{key}";
var commandVideo = commandBase + $" \"{tempTsFile}.video.enc.m4s\" \"{tempTsFile}.video.m4s\"";
var commandAudio = commandBase + $" \"{tempTsFile}.audio.enc.m4s\" \"{tempTsFile}.audio.m4s\"";
var tempTsFileName = Path.GetFileName(tempTsFile);
var tempTsFileWorkDir = Path.GetDirectoryName(tempTsFile) ?? CfgManager.PathVIDEOS_DIR;
var commandVideo = commandBase + $" \"{tempTsFileName}.video.enc.m4s\" \"{tempTsFileName}.video.m4s\"";
var commandAudio = commandBase + $" \"{tempTsFileName}.audio.enc.m4s\" \"{tempTsFileName}.audio.m4s\"";
if (videoDownloaded){
Console.WriteLine("Started decrypting video");
data.DownloadProgress = new DownloadProgress(){
@ -1206,7 +1208,7 @@ public class Crunchyroll{
Doing = "Decrypting video"
};
Queue.Refresh();
var decryptVideo = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo);
var decryptVideo = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandVideo,tempTsFileWorkDir);
if (!decryptVideo.IsOk){
Console.Error.WriteLine($"Decryption failed with exit code {decryptVideo.ErrorCode}");
@ -1216,7 +1218,14 @@ public class Crunchyroll{
} catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}");
}
} else{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "Decryption failed"
};
}
Console.WriteLine("Decryption done for video");
if (!options.Nocleanup){
try{
@ -1249,7 +1258,6 @@ public class Crunchyroll{
Lang = lang.Value,
IsPrimary = isPrimary
});
}
} else{
Console.WriteLine("No Video downloaded");
}
@ -1264,7 +1272,7 @@ public class Crunchyroll{
Doing = "Decrypting audio"
};
Queue.Refresh();
var decryptAudio = await Helpers.ExecuteCommandAsync("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio);
var decryptAudio = await Helpers.ExecuteCommandAsyncWorkDir("mp4decrypt", CfgManager.PathMP4Decrypt, commandAudio,tempTsFileWorkDir);
if (!decryptAudio.IsOk){
Console.Error.WriteLine($"Decryption failed with exit code {decryptAudio.ErrorCode}");
@ -1273,7 +1281,14 @@ public class Crunchyroll{
} catch (IOException ex){
Console.WriteLine($"An error occurred: {ex.Message}");
}
} else{
return new DownloadResponse{
Data = files,
Error = dlFailed,
FileName = fileName.Length > 0 ? (Path.IsPathRooted(fileName) ? fileName : Path.Combine(fileDir, fileName)) : "./unknown",
ErrorText = "Decryption failed"
};
}
Console.WriteLine("Decryption done for audio");
if (!options.Nocleanup){
try{
@ -1306,7 +1321,6 @@ public class Crunchyroll{
Lang = lang.Value,
IsPrimary = isPrimary
});
}
} else{
Console.WriteLine("No Audio downloaded");
}

View File

@ -3,9 +3,11 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Runtime.Serialization;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Avalonia.Media.Imaging;
using Newtonsoft.Json;
namespace CRD.Utils;
@ -194,6 +196,47 @@ public class Helpers{
}
}
public static async Task<(bool IsOk, int ErrorCode)> ExecuteCommandAsyncWorkDir(string type, string bin, string command,string workingDir){
try{
using (var process = new Process()){
process.StartInfo.WorkingDirectory = workingDir;
process.StartInfo.FileName = bin;
process.StartInfo.Arguments = command;
process.StartInfo.RedirectStandardOutput = true;
process.StartInfo.RedirectStandardError = true;
process.StartInfo.UseShellExecute = false;
process.StartInfo.CreateNoWindow = true;
process.OutputDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine(e.Data);
}
};
process.ErrorDataReceived += (sender, e) => {
if (!string.IsNullOrEmpty(e.Data)){
Console.WriteLine($"{e.Data}");
}
};
process.Start();
process.BeginOutputReadLine();
process.BeginErrorReadLine();
await process.WaitForExitAsync();
// Define success condition more appropriately based on the application
bool isSuccess = process.ExitCode == 0;
return (IsOk: isSuccess, ErrorCode: process.ExitCode);
}
} catch (Exception ex){
Console.Error.WriteLine($"An error occurred: {ex.Message}");
return (IsOk: false, ErrorCode: -1);
}
}
public static double CalculateCosineSimilarity(string text1, string text2){
var vector1 = ComputeWordFrequency(text1);
var vector2 = ComputeWordFrequency(text2);
@ -272,4 +315,23 @@ public class Helpers{
return null;
}
}
public static async Task<Bitmap?> LoadImage(string imageUrl){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(imageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
return new Bitmap(stream);
}
}
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
return null;
}
}

View File

@ -123,6 +123,7 @@ public static class Api{
public static readonly string BetaProfile = ApiBeta + "/accounts/v1/me/profile";
public static readonly string BetaCmsToken = ApiBeta + "/index/v2";
public static readonly string Search = ApiBeta + "/content/v2/discover/search";
public static readonly string Browse = ApiBeta + "/content/v2/discover/browse";
public static readonly string Cms = ApiBeta + "/content/v2/cms";
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
public static readonly string BetaCms = ApiBeta + "/cms/v2";

View File

@ -26,7 +26,7 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
public DateTime? DateTime{ get; set; }
public bool? HasPassed{ get; set; }
public string? EpisodeName{ get; set; }
public string? SeasonUrl{ get; set; }
public string? SeriesUrl{ get; set; }
public string? EpisodeUrl{ get; set; }
public string? ThumbnailUrl{ get; set; }
public Bitmap? ImageBitmap{ get; set; }

View File

@ -0,0 +1,205 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Media.Imaging;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public class CrBrowseEpisodeBase{
public int Total{ get; set; }
public List<CrBrowseEpisode>? Data{ get; set; }
public Meta Meta{ get; set; }
}
public class CrBrowseEpisode : INotifyPropertyChanged{
[JsonProperty("external_id")]
public string? ExternalId{ get; set; }
[JsonProperty("last_public")]
public DateTime LastPublic{ get; set; }
public string? Description{ get; set; }
public bool New{ get; set; }
[JsonProperty("linked_resource_key")]
public string? LinkedResourceKey{ get; set; }
[JsonProperty("slug_title")]
public string? SlugTitle{ get; set; }
public string? Title{ get; set; }
[JsonProperty("promo_title")]
public string? PromoTitle{ get; set; }
[JsonProperty("episode_metadata")]
public CrBrowseEpisodeMetaData EpisodeMetadata{ get; set; }
public string? Id{ get; set; }
public Images Images{ get; set; }
[JsonProperty("promo_description")]
public string? PromoDescription{ get; set; }
public string? Slug{ get; set; }
public string? Type{ get; set; }
[JsonProperty("channel_id")]
public string? ChannelId{ get; set; }
[JsonProperty("streams_link")]
public string? StreamsLink{ get; set; }
[JsonIgnore]
public Bitmap? ImageBitmap{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}
public class CrBrowseEpisodeMetaData{
[JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; }
[JsonProperty("content_descriptors")]
public List<string>? ContentDescriptors{ get; set; }
[JsonProperty("availability_notes")]
public string? AvailabilityNotes{ get; set; }
public string? Episode{ get; set; }
[JsonProperty("episode_air_date")]
public DateTime EpisodeAirDate{ get; set; }
[JsonProperty("episode_number")]
public int EpisodeCount{ get; set; }
[JsonProperty("duration_ms")]
public int DurationMs{ get; set; }
[JsonProperty("extended_maturity_rating")]
public Dictionary<object, object>?
ExtendedMaturityRating{ get; set; }
[JsonProperty("is_dubbed")]
public bool IsDubbed{ get; set; }
[JsonProperty("is_mature")]
public bool IsMature{ get; set; }
[JsonProperty("is_subbed")]
public bool IsSubbed{ get; set; }
[JsonProperty("mature_blocked")]
public bool MatureBlocked{ get; set; }
[JsonProperty("is_premium_only")]
public bool IsPremiumOnly{ get; set; }
[JsonProperty("is_clip")]
public bool IsClip{ get; set; }
[JsonProperty("maturity_ratings")]
public List<string>? MaturityRatings{ get; set; }
[JsonProperty("season_number")]
public double SeasonNumber{ get; set; }
[JsonProperty("season_sequence_number")]
public double SeasonSequenceNumber{ get; set; }
[JsonProperty("sequence_number")]
public double SequenceNumber{ get; set; }
[JsonProperty("upload_date")]
public DateTime? UploadDate{ get; set; }
[JsonProperty("subtitle_locales")]
public List<Locale>? SubtitleLocales{ get; set; }
[JsonProperty("premium_available_date")]
public DateTime? PremiumAvailableDate{ get; set; }
[JsonProperty("availability_ends")]
public DateTime? AvailabilityEnds{ get; set; }
[JsonProperty("availability_starts")]
public DateTime? AvailabilityStarts{ get; set; }
[JsonProperty("free_available_date")]
public DateTime? FreeAvailableDate{ get; set; }
[JsonProperty("identifier")]
public string? Identifier{ get; set; }
[JsonProperty("season_id")]
public string? SeasonId{ get; set; }
[JsonProperty("series_id")]
public string? SeriesId{ get; set; }
[JsonProperty("season_display_number")]
public string? SeasonDisplayNumber{ get; set; }
[JsonProperty("eligible_region")]
public string? EligibleRegion{ get; set; }
[JsonProperty("available_date")]
public DateTime? AvailableDate{ get; set; }
[JsonProperty("premium_date")]
public DateTime? PremiumDate{ get; set; }
[JsonProperty("available_offline")]
public bool AvailableOffline{ get; set; }
[JsonProperty("closed_captions_available")]
public bool ClosedCaptionsAvailable{ get; set; }
[JsonProperty("season_slug_title")]
public string? SeasonSlugTitle{ get; set; }
[JsonProperty("season_title")]
public string? SeasonTitle{ get; set; }
[JsonProperty("series_slug_title")]
public string? SeriesSlugTitle{ get; set; }
[JsonProperty("series_title")]
public string? SeriesTitle{ get; set; }
[JsonProperty("versions")]
public List<CrBrowseEpisodeVersion>? versions{ get; set; }
}
public struct CrBrowseEpisodeVersion{
[JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; }
public string? Guid{ get; set; }
public bool? Original{ get; set; }
public string? Variant{ get; set; }
[JsonProperty("season_guid")]
public string? SeasonGuid{ get; set; }
[JsonProperty("media_guid")]
public string? MediaGuid{ get; set; }
[JsonProperty("is_premium_only")]
public bool? IsPremiumOnly{ get; set; }
}

View File

@ -0,0 +1,117 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Media.Imaging;
using Newtonsoft.Json;
namespace CRD.Utils.Structs;
public class CrBrowseSeriesBase{
public int Total{ get; set; }
public List<CrBrowseSeries>? Data{ get; set; }
public Meta Meta{ get; set; }
}
public class CrBrowseSeries : INotifyPropertyChanged{
[JsonProperty("external_id")]
public string? ExternalId{ get; set; }
[JsonProperty("last_public")]
public DateTime LastPublic{ get; set; }
public string? Description{ get; set; }
public bool New{ get; set; }
[JsonProperty("linked_resource_key")]
public string? LinkedResourceKey{ get; set; }
[JsonProperty("slug_title")]
public string? SlugTitle{ get; set; }
public string? Title{ get; set; }
[JsonProperty("promo_title")]
public string? PromoTitle{ get; set; }
[JsonProperty("series_metadata")]
public CrBrowseSeriesMetaData SeriesMetadata{ get; set; }
public string? Id{ get; set; }
public Images Images{ get; set; }
[JsonProperty("promo_description")]
public string? PromoDescription{ get; set; }
public string? Slug{ get; set; }
public string? Type{ get; set; }
[JsonProperty("channel_id")]
public string? ChannelId{ get; set; }
[JsonIgnore]
public Bitmap? ImageBitmap{ get; set; }
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}
public class CrBrowseSeriesMetaData{
[JsonProperty("audio_locales")]
public List<Locale>? AudioLocales{ get; set; }
[JsonProperty("awards")]
public List<object> awards{ get; set; }
[JsonProperty("availability_notes")]
public string? AvailabilityNotes{ get; set; }
[JsonProperty("content_descriptors")]
public List<string>? ContentDescriptors{ get; set; }
[JsonProperty("episode_count")]
public int EpisodeCount{ get; set; }
[JsonProperty("extended_description")]
public string? ExtendedDescription{ get; set; }
[JsonProperty("extended_maturity_rating")]
public Dictionary<object, object>?
ExtendedMaturityRating{ get; set; }
[JsonProperty("is_dubbed")]
public bool IsDubbed{ get; set; }
[JsonProperty("is_mature")]
public bool IsMature{ get; set; }
[JsonProperty("is_simulcast")]
public bool IsSimulcast{ get; set; }
[JsonProperty("is_subbed")]
public bool IsSubbed{ get; set; }
[JsonProperty("mature_blocked")]
public bool MatureBlocked{ get; set; }
[JsonProperty("maturity_ratings")]
public List<string>? MaturityRatings{ get; set; }
[JsonProperty("season_count")]
public int SeasonCount{ get; set; }
[JsonProperty("series_launch_year")]
public int SeriesLaunchYear{ get; set; }
[JsonProperty("subtitle_locales")]
public List<Locale>? SubtitleLocales{ get; set; }
[JsonProperty("tenant_categories")]
public List<string>? TenantCategories{ get; set; }
}

View File

@ -0,0 +1,15 @@
using System.Collections.Generic;
namespace CRD.Utils.Structs;
public class CrSearchSeries{
public int count{ get; set; }
public List<CrBrowseSeries>? Items{ get; set; }
public string? type{ get; set; }
}
public class CrSearchSeriesBase{
public int Total{ get; set; }
public List<CrSearchSeries>? Data{ get; set; }
public Meta Meta{ get; set; }
}

View File

@ -11,48 +11,90 @@ public class CrSeriesSearch{
public struct SeriesSearchItem{
public string Description{ get; set; }
[JsonProperty("seo_description")] public string SeoDescription{ get; set; }
[JsonProperty("number_of_episodes")] public int NumberOfEpisodes{ get; set; }
[JsonProperty("is_dubbed")] public bool IsDubbed{ get; set; }
[JsonProperty("seo_description")]
public string SeoDescription{ get; set; }
[JsonProperty("number_of_episodes")]
public int NumberOfEpisodes{ get; set; }
[JsonProperty("is_dubbed")]
public bool IsDubbed{ get; set; }
public string Identifier{ get; set; }
[JsonProperty("channel_id")] public string ChannelId{ get; set; }
[JsonProperty("slug_title")] public string SlugTitle{ get; set; }
[JsonProperty("channel_id")]
public string ChannelId{ get; set; }
[JsonProperty("slug_title")]
public string SlugTitle{ get; set; }
[JsonProperty("season_sequence_number")]
public int SeasonSequenceNumber{ get; set; }
[JsonProperty("season_tags")] public List<string> SeasonTags{ get; set; }
[JsonProperty("season_tags")]
public List<string> SeasonTags{ get; set; }
[JsonProperty("extended_maturity_rating")]
public Dictionary<object, object>
ExtendedMaturityRating{ get; set; }
[JsonProperty("is_mature")] public bool IsMature{ get; set; }
[JsonProperty("audio_locale")] public string AudioLocale{ get; set; }
[JsonProperty("season_number")] public int SeasonNumber{ get; set; }
[JsonProperty("is_mature")]
public bool IsMature{ get; set; }
[JsonProperty("audio_locale")]
public string AudioLocale{ get; set; }
[JsonProperty("season_number")]
public int SeasonNumber{ get; set; }
public Dictionary<object, object> Images{ get; set; }
[JsonProperty("mature_blocked")] public bool MatureBlocked{ get; set; }
[JsonProperty("mature_blocked")]
public bool MatureBlocked{ get; set; }
public List<Version>? Versions{ get; set; }
public string Title{ get; set; }
[JsonProperty("is_subbed")] public bool IsSubbed{ get; set; }
[JsonProperty("is_subbed")]
public bool IsSubbed{ get; set; }
public string Id{ get; set; }
[JsonProperty("audio_locales")] public List<string> AudioLocales{ get; set; }
[JsonProperty("subtitle_locales")] public List<string> SubtitleLocales{ get; set; }
[JsonProperty("availability_notes")] public string AvailabilityNotes{ get; set; }
[JsonProperty("series_id")] public string SeriesId{ get; set; }
[JsonProperty("audio_locales")]
public List<string> AudioLocales{ get; set; }
[JsonProperty("subtitle_locales")]
public List<string> SubtitleLocales{ get; set; }
[JsonProperty("availability_notes")]
public string AvailabilityNotes{ get; set; }
[JsonProperty("series_id")]
public string SeriesId{ get; set; }
[JsonProperty("season_display_number")]
public string SeasonDisplayNumber{ get; set; }
[JsonProperty("is_complete")] public bool IsComplete{ get; set; }
[JsonProperty("is_complete")]
public bool IsComplete{ get; set; }
public List<string> Keywords{ get; set; }
[JsonProperty("maturity_ratings")] public List<string> MaturityRatings{ get; set; }
[JsonProperty("is_simulcast")] public bool IsSimulcast{ get; set; }
[JsonProperty("seo_title")] public string SeoTitle{ get; set; }
[JsonProperty("maturity_ratings")]
public List<string> MaturityRatings{ get; set; }
[JsonProperty("is_simulcast")]
public bool IsSimulcast{ get; set; }
[JsonProperty("seo_title")]
public string SeoTitle{ get; set; }
}
public struct Version{
[JsonProperty("audio_locale")] public string? AudioLocale{ get; set; }
[JsonProperty("audio_locale")]
public string? AudioLocale{ get; set; }
public string? Guid{ get; set; }
public bool? Original{ get; set; }
public string? Variant{ get; set; }

View File

@ -10,20 +10,34 @@ public class PlaybackData{
}
public class StreamDetails{
[JsonProperty("hardsub_locale")] public Locale? HardsubLocale{ get; set; }
[JsonProperty("hardsub_locale")]
public Locale? HardsubLocale{ get; set; }
public string? Url{ get; set; }
[JsonProperty("hardsub_lang")] public string? HardsubLang{ get; set; }
[JsonProperty("audio_lang")] public string? AudioLang{ get; set; }
[JsonProperty("hardsub_lang")]
public string? HardsubLang{ get; set; }
[JsonProperty("audio_lang")]
public string? AudioLang{ get; set; }
public string? Type{ get; set; }
}
public class PlaybackMeta{
[JsonProperty("media_id")] public string? MediaId{ get; set; }
[JsonProperty("media_id")]
public string? MediaId{ get; set; }
public Subtitles? Subtitles{ get; set; }
public List<string>? Bifs{ get; set; }
public List<PlaybackVersion>? Versions{ get; set; }
[JsonProperty("audio_locale")] public Locale? AudioLocale{ get; set; }
[JsonProperty("closed_captions")] public Subtitles? ClosedCaptions{ get; set; }
[JsonProperty("audio_locale")]
public Locale? AudioLocale{ get; set; }
[JsonProperty("closed_captions")]
public Subtitles? ClosedCaptions{ get; set; }
public Dictionary<string, Caption>? Captions{ get; set; }
}
@ -38,12 +52,22 @@ public class CrunchyStreams : Dictionary<string, StreamDetails>;
public class Subtitles : Dictionary<string, SubtitleInfo>;
public class PlaybackVersion{
[JsonProperty("audio_locale")] public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere
[JsonProperty("audio_locale")]
public Locale AudioLocale{ get; set; } // Assuming Locale is defined elsewhere
public string? Guid{ get; set; }
[JsonProperty("is_premium_only")] public bool IsPremiumOnly{ get; set; }
[JsonProperty("media_guid")] public string? MediaGuid{ get; set; }
[JsonProperty("is_premium_only")]
public bool IsPremiumOnly{ get; set; }
[JsonProperty("media_guid")]
public string? MediaGuid{ get; set; }
public bool Original{ get; set; }
[JsonProperty("season_guid")] public string? SeasonGuid{ get; set; }
[JsonProperty("season_guid")]
public string? SeasonGuid{ get; set; }
public string? Variant{ get; set; }
}

View File

@ -2,8 +2,11 @@ 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.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
@ -13,6 +16,7 @@ using CRD.Downloader;
using CRD.Utils;
using CRD.Utils.Structs;
using CRD.Views;
using DynamicData;
using ReactiveUI;
@ -37,9 +41,22 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
[ObservableProperty]
public bool _showLoading = false;
[ObservableProperty]
public bool _searchEnabled = false;
[ObservableProperty]
public bool _searchVisible = true;
[ObservableProperty]
public bool _searchPopupVisible = false;
public ObservableCollection<ItemModel> Items{ get; } = new();
public ObservableCollection<CrBrowseSeries> SearchItems{ get; set; } = new();
public ObservableCollection<ItemModel> SelectedItems{ get; } = new();
[ObservableProperty]
public CrBrowseSeries _selectedSearchItem;
[ObservableProperty]
public ComboBoxItem _currentSelectedSeason;
@ -51,26 +68,75 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
private CrunchySeriesList? currentSeriesList;
private readonly SemaphoreSlim _updateSearchSemaphore = new SemaphoreSlim(1, 1);
public AddDownloadPageViewModel(){
SelectedItems.CollectionChanged += OnSelectedItemsChanged;
}
private async Task UpdateSearch(string value){
var searchResults = await Crunchyroll.Instance.CrSeries.Search(value, "");
var searchItems = searchResults?.Data?.First().Items;
SearchItems.Clear();
if (searchItems is{ Count: > 0 }){
foreach (var episode in searchItems){
if (episode.ImageBitmap == null){
if (episode.Images.PosterTall != null){
var posterTall = episode.Images.PosterTall.First();
var imageUrl = posterTall.Find(ele => ele.Height == 180).Source
?? (posterTall.Count >= 2 ? posterTall[1].Source : posterTall.FirstOrDefault().Source);
episode.LoadImage(imageUrl);
}
}
SearchItems.Add(episode);
}
SearchPopupVisible = true;
RaisePropertyChanged(nameof(SearchItems));
RaisePropertyChanged(nameof(SearchVisible));
return;
}
SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
}
partial void OnUrlInputChanged(string value){
if (UrlInput.Length > 9){
if (SearchEnabled){
UpdateSearch(value);
ButtonText = "Select Searched Series";
ButtonEnabled = false;
} else if (UrlInput.Length > 9){
if (UrlInput.Contains("/watch/concert/") || UrlInput.Contains("/artist/")){
MessageBus.Current.SendMessage(new ToastMessage("Concerts / Artists not implemented yet", ToastType.Error, 1));
} else if (UrlInput.Contains("/watch/")){
//Episode
ButtonText = "Add Episode to Queue";
ButtonEnabled = true;
SearchVisible = false;
} else if (UrlInput.Contains("/series/")){
//Series
ButtonText = "List Episodes";
ButtonEnabled = true;
SearchVisible = false;
} else{
ButtonEnabled = false;
SearchVisible = true;
}
} else{
ButtonText = "Enter Url";
ButtonEnabled = false;
SearchVisible = true;
}
}
partial void OnSearchEnabledChanged(bool value){
if (SearchEnabled){
ButtonText = "Select Searched Series";
ButtonEnabled = false;
} else{
ButtonText = "Enter Url";
ButtonEnabled = false;
@ -106,6 +172,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
AddAllEpisodes = false;
ButtonText = "Enter Url";
ButtonEnabled = false;
SearchVisible = true;
} else if (UrlInput.Length > 9){
episodesBySeason.Clear();
SeasonList.Clear();
@ -208,7 +275,43 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
async partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){
async partial void OnSelectedSearchItemChanged(CrBrowseSeries value){
if (value == null){
return;
}
SearchPopupVisible = false;
RaisePropertyChanged(nameof(SearchVisible));
SearchItems.Clear();
SearchVisible = false;
ButtonEnabled = false;
ShowLoading = true;
var list = await Crunchyroll.Instance.CrSeries.ListSeriesId(value.Id, "", new CrunchyMultiDownload(Crunchyroll.Instance.CrunOptions.DubLang, true));
ShowLoading = false;
if (list != null){
currentSeriesList = list;
foreach (var episode in currentSeriesList.Value.List){
if (episodesBySeason.ContainsKey("S" + episode.Season)){
episodesBySeason["S" + episode.Season].Add(new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E,
episode.Lang));
} else{
episodesBySeason.Add("S" + episode.Season, new List<ItemModel>{
new ItemModel(episode.Img, episode.Description, episode.Time, episode.Name, "S" + episode.Season, "E" + episode.EpisodeNum, episode.E, episode.Lang)
});
SeasonList.Add(new ComboBoxItem{ Content = "S" + episode.Season });
}
}
CurrentSelectedSeason = SeasonList[0];
ButtonEnabled = false;
AllButtonEnabled = true;
ButtonText = "Select Episodes";
} else{
ButtonEnabled = true;
}
}
partial void OnCurrentSelectedSeasonChanged(ComboBoxItem? value){
if (value == null){
return;
}
@ -218,7 +321,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
if (episodesBySeason.TryGetValue(key, out var season)){
foreach (var episode in season){
if (episode.ImageBitmap == null){
await episode.LoadImage();
episode.LoadImage(episode.ImageUrl);
Items.Add(episode);
if (selectedEpisodes.Contains(episode.AbsolutNum)){
SelectedItems.Add(episode);
@ -234,7 +337,7 @@ public partial class AddDownloadPageViewModel : ViewModelBase{
}
}
public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios){
public class ItemModel(string imageUrl, string description, string time, string title, string season, string episode, string absolutNum, List<string> availableAudios) : INotifyPropertyChanged{
public string ImageUrl{ get; set; } = imageUrl;
public Bitmap? ImageBitmap{ get; set; }
public string Title{ get; set; } = title;
@ -249,18 +352,10 @@ public class ItemModel(string imageUrl, string description, string time, string
public List<string> AvailableAudios{ get; set; } = availableAudios;
public async Task LoadImage(){
try{
using (var client = new HttpClient()){
var response = await client.GetAsync(ImageUrl);
response.EnsureSuccessStatusCode();
using (var stream = await response.Content.ReadAsStreamAsync()){
ImageBitmap = new Bitmap(stream);
}
}
} catch (Exception ex){
// Handle exceptions
Console.Error.WriteLine("Failed to load image: " + ex.Message);
}
public event PropertyChangedEventHandler? PropertyChanged;
public async void LoadImage(string url){
ImageBitmap = await Helpers.LoadImage(url);
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ImageBitmap)));
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Avalonia.Controls;
@ -14,8 +15,14 @@ namespace CRD.ViewModels;
public partial class CalendarPageViewModel : ViewModelBase{
public ObservableCollection<CalendarDay> CalendarDays{ get; set; }
[ObservableProperty] private ComboBoxItem? _currentCalendarLanguage;
[ObservableProperty] private bool? _showLoading = false;
[ObservableProperty]
private ComboBoxItem? _currentCalendarLanguage;
[ObservableProperty]
private bool _showLoading = false;
[ObservableProperty]
private bool _customCalendar = false;
public ObservableCollection<ComboBoxItem> CalendarLanguage{ get; } = new(){
new ComboBoxItem(){ Content = "en-us" },
@ -68,6 +75,7 @@ public partial class CalendarPageViewModel : ViewModelBase{
ShowLoading = false;
return;
}
currentWeek = week;
CalendarDays.Clear();
CalendarDays.AddRange(week.CalendarDays);
@ -95,6 +103,12 @@ public partial class CalendarPageViewModel : ViewModelBase{
[RelayCommand]
public void Refresh(){
if (CustomCalendar){
BuildCustomCalendar();
return;
}
string mondayDate;
if (currentWeek is{ FirstDayOfWeekString: not null }){
@ -140,4 +154,82 @@ public partial class CalendarPageViewModel : ViewModelBase{
CfgManager.WriteSettingsToFile();
}
}
partial void OnCustomCalendarChanged(bool value){
if (CustomCalendar){
BuildCustomCalendar();
}
}
private async void BuildCustomCalendar(){
ShowLoading = true;
var newEpisodesBase = await Crunchyroll.Instance.CrEpisode.GetNewEpisodes(Crunchyroll.Instance.CrunOptions.HistoryLang);
CalendarWeek week = new CalendarWeek();
week.CalendarDays = new List<CalendarDay>();
DateTime today = DateTime.Now;
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();
week.CalendarDays.Add(calDay);
}
week.CalendarDays.Reverse();
if (newEpisodesBase is{ Data.Count: > 0 }){
var newEpisodes = newEpisodesBase.Data;
foreach (var crBrowseEpisode in newEpisodes){
var episodeAirDate = crBrowseEpisode.EpisodeMetadata.EpisodeAirDate;
if (crBrowseEpisode.EpisodeMetadata.SeasonTitle != null && crBrowseEpisode.EpisodeMetadata.SeasonTitle.EndsWith("Dub)")){
continue;
}
var calendarDay = week.CalendarDays.FirstOrDefault(day => day.DateTime != null && day.DateTime.Value.Date == episodeAirDate.Date);
if (calendarDay != null){
CalendarEpisode calEpisode = new CalendarEpisode();
calEpisode.DateTime = episodeAirDate;
calEpisode.HasPassed = DateTime.Now > episodeAirDate;
calEpisode.EpisodeName = crBrowseEpisode.Title;
calEpisode.SeriesUrl = "https://www.crunchyroll.com/series/" + crBrowseEpisode.EpisodeMetadata.SeriesId;
calEpisode.EpisodeUrl = $"https://www.crunchyroll.com/de/watch/{crBrowseEpisode.Id}/";
calEpisode.ThumbnailUrl = crBrowseEpisode.Images.Thumbnail.First().First().Source;
calEpisode.IsPremiumOnly = crBrowseEpisode.EpisodeMetadata.IsPremiumOnly;
calEpisode.IsPremiere = crBrowseEpisode.EpisodeMetadata.Episode == "1";
calEpisode.SeasonName = crBrowseEpisode.EpisodeMetadata.SeasonTitle;
calEpisode.EpisodeNumber = crBrowseEpisode.EpisodeMetadata.Episode;
calendarDay.CalendarEpisodes?.Add(calEpisode);
}
}
}
foreach (var day in week.CalendarDays){
if (day.CalendarEpisodes != null) day.CalendarEpisodes = day.CalendarEpisodes.OrderBy(e => e.DateTime).ToList();
}
currentWeek = week;
CalendarDays.Clear();
CalendarDays.AddRange(week.CalendarDays);
RaisePropertyChanged(nameof(CalendarDays));
ShowLoading = false;
foreach (var calendarDay in CalendarDays){
foreach (var calendarDayCalendarEpisode in calendarDay.CalendarEpisodes){
if (calendarDayCalendarEpisode.ImageBitmap == null){
calendarDayCalendarEpisode.LoadImage();
}
}
}
}
}

View File

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:vm="clr-namespace:CRD.ViewModels"
xmlns:structs="clr-namespace:CRD.Utils.Structs"
x:DataType="vm:AddDownloadPageViewModel"
x:Class="CRD.Views.AddDownloadPageView">
@ -19,10 +20,83 @@
</Grid.RowDefinitions>
<!-- Text Input Field -->
<TextBox Grid.Row="0" Watermark="Enter series or episode url" Text="{Binding UrlInput}" Margin="10"
<StackPanel Grid.Row="0" Orientation="Vertical">
<TextBox x:Name="SearchBar" Watermark="Enter series or episode url" Text="{Binding UrlInput}" Margin="10"
VerticalAlignment="Top" />
<Popup IsLightDismissEnabled="False"
MaxWidth="{Binding Bounds.Width, ElementName=SearchBar}"
MaxHeight="{Binding Bounds.Height, ElementName=Grid}"
IsOpen="{Binding SearchPopupVisible}"
Placement="Bottom"
PlacementTarget="{Binding ElementName=SearchBar}">
<Border BorderThickness="1" Background="{DynamicResource ComboBoxDropDownBackground}">
<ListBox x:Name="ListBoxDubsSelection"
ItemsSource="{Binding SearchItems}"
SelectedItem="{Binding SelectedSearchItem}">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type structs:CrBrowseSeries}">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid Grid.Row="1" Grid.Column="0">
<!-- Define a row with auto height to match the image height -->
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Define columns if needed -->
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Image Margin="10"
Source="{Binding ImageBitmap}"
Width="120"
Height="180" />
</Grid>
<Grid Grid.Row="1" Grid.Column="1">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding Title}"></TextBlock>
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
Text="{Binding Description}">
</TextBlock>
</Grid>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
</Popup>
</StackPanel>
<Grid Grid.Row="1" Margin="10 0 10 0">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
@ -36,10 +110,17 @@
Content="{Binding ButtonText}">
</Button>
<CheckBox Grid.Column="1" IsEnabled="{Binding AllButtonEnabled}" IsChecked="{Binding AddAllEpisodes}"
<StackPanel Grid.Column="1" Orientation="Horizontal">
<CheckBox IsEnabled="{Binding AllButtonEnabled}" IsChecked="{Binding AddAllEpisodes}"
Content="All" Margin="5 0 0 0">
</CheckBox>
<CheckBox IsVisible="{Binding SearchVisible}" IsChecked="{Binding SearchEnabled}"
Content="Search" Margin="5 0 0 0">
</CheckBox>
</StackPanel>
<!-- ComboBox -->
<ComboBox Grid.Column="2" MinWidth="200" SelectedItem="{Binding CurrentSelectedSeason}"
ItemsSource="{Binding SeasonList}">
@ -54,14 +135,13 @@
Value="50"
Maximum="100"
MaxWidth="100"
IsVisible="{Binding ShowLoading}"
>
IsVisible="{Binding ShowLoading}">
</ProgressBar>
</Grid>
<!-- ListBox with Custom Elements -->
<ListBox Grid.Row="2" Margin="10" SelectionMode="Multiple,Toggle" VerticalAlignment="Stretch"
SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}">
SelectedItems="{Binding SelectedItems}" ItemsSource="{Binding Items}" x:Name="Grid">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type vm:ItemModel}">
<StackPanel>

View File

@ -27,6 +27,7 @@
</Grid.ColumnDefinitions>
<Button Grid.Row="0" Grid.Column="0" Margin="10 10 0 0" HorizontalAlignment="Center"
IsEnabled="{Binding !CustomCalendar}"
Command="{Binding PrevWeek}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ChevronLeft" FontSize="18" />
@ -44,10 +45,14 @@
SelectedItem="{Binding CurrentCalendarLanguage}"
ItemsSource="{Binding CalendarLanguage}">
</ComboBox>
<CheckBox IsChecked="{Binding CustomCalendar}"
Content="Custom Calendar" Margin="5 0 0 0">
</CheckBox>
</StackPanel>
<Button Grid.Row="0" Grid.Column="2" Margin="0 0 10 0" HorizontalAlignment="Center"
IsEnabled="{Binding !CustomCalendar}"
Command="{Binding NextWeek}">
<StackPanel Orientation="Horizontal">
<controls:SymbolIcon Symbol="ChevronRight" FontSize="18" />