mirror of
https://github.com/Crunchy-DL/Crunchy-Downloader.git
synced 2026-01-11 20:10:26 +00:00
Add - Added option to **mux fonts into the MKV**
Add - Added **completion sound** when downloads finish, allowing a specified sound to be played Add - Added **changelog** for easier tracking of changes and updates Add - Added **Retry button** to reset all failed downloads, allowing "Auto Download" to restart them Add - Added **dub/sub info** to the Upcoming tab Chg - Changed **history table view** to include missing features from the poster view Chg - Changed and **adjusted error messages** Chg - Changed **chapters formatting** for consistent output across locales Chg - Changed **Clear Queue** button to icon-only Chg - Changed **update button** behavior Chg - Changed some **error messages** for better debugging Chg - Changed **history series access**, now allowing it to open while others are refreshing Chg - Changed **device ID reuse** to fix continuous login emails Chg - Changed **authentication log** to write the full message for better debugging Chg - Updated **dependencies** Fix - Temporary fix for **authentication issues** Fix - Fixed **unable to download movies** [#237](https://github.com/Crunchy-DL/Crunchy-Downloader/issues/237) Fix - Fixed **buggy queue behavior** during active downloads Fix - Fixed **duplicate seasons in history** when adding multiple episodes from the calendar Fix - Fixed crash if **all** subtitles option is selected Fix - Fixed "**Cannot set download directory to Drive**" https://github.com/Crunchy-DL/Crunchy-Downloader/issues/220 Fix - Fixed missing **subtitles and none subs** for some series
This commit is contained in:
parent
40b7b6d1de
commit
aca28a4e17
43 changed files with 1372 additions and 446 deletions
|
|
@ -81,8 +81,8 @@ public class CrAuth{
|
|||
string uuid = Guid.NewGuid().ToString();
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "username", data.Username },
|
||||
{ "password", data.Password },
|
||||
{ "username", data.Username },
|
||||
{ "password", data.Password },
|
||||
{ "grant_type", "password" },
|
||||
{ "scope", "offline_access" },
|
||||
{ "device_id", uuid },
|
||||
|
|
@ -115,9 +115,16 @@ public class CrAuth{
|
|||
} else{
|
||||
if (response.ResponseContent.Contains("invalid_credentials")){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - because of invalid login credentials", ToastType.Error, 10));
|
||||
} else if (response.ResponseContent.Contains("<title>Just a moment...</title>") ||
|
||||
response.ResponseContent.Contains("<title>Access denied</title>") ||
|
||||
response.ResponseContent.Contains("<title>Attention Required! | Cloudflare</title>") ||
|
||||
response.ResponseContent.Trim().Equals("error code: 1020") ||
|
||||
response.ResponseContent.IndexOf("<title>DDOS-GUARD</title>", StringComparison.OrdinalIgnoreCase) > -1){
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - Cloudflare error try to change to BetaAPI in settings", ToastType.Error, 10));
|
||||
} else{
|
||||
MessageBus.Current.SendMessage(new ToastMessage($"Failed to login - {response.ResponseContent.Substring(0, response.ResponseContent.Length < 200 ? response.ResponseContent.Length : 200)}",
|
||||
ToastType.Error, 10));
|
||||
await Console.Error.WriteLineAsync("Full Response: " + response.ResponseContent);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +198,7 @@ public class CrAuth{
|
|||
return;
|
||||
}
|
||||
|
||||
string uuid = Guid.NewGuid().ToString();
|
||||
string uuid = string.IsNullOrEmpty(crunInstance.Token.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id;
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "refresh_token", crunInstance.Token.refresh_token },
|
||||
|
|
@ -252,7 +259,7 @@ public class CrAuth{
|
|||
return;
|
||||
}
|
||||
|
||||
string uuid = Guid.NewGuid().ToString();
|
||||
string uuid = string.IsNullOrEmpty(crunInstance.Token?.device_id) ? Guid.NewGuid().ToString() : crunInstance.Token.device_id;
|
||||
|
||||
var formData = new Dictionary<string, string>{
|
||||
{ "refresh_token", crunInstance.Token?.refresh_token ?? "" },
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ public class CrMovies{
|
|||
}
|
||||
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/movies/{id}", HttpMethod.Get, true, true, query);
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Cms}/objects/{id}", HttpMethod.Get, true, true, query);
|
||||
|
||||
var response = await HttpClientReq.Instance.SendHttpRequest(request);
|
||||
|
||||
|
|
|
|||
|
|
@ -454,7 +454,7 @@ public class CrSeries{
|
|||
|
||||
query["q"] = searchString;
|
||||
query["n"] = "6";
|
||||
query["type"] = "top_results";
|
||||
query["type"] = "series";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.Search}", HttpMethod.Get, true, false, query);
|
||||
|
||||
|
|
@ -525,4 +525,29 @@ public class CrSeries{
|
|||
|
||||
return complete;
|
||||
}
|
||||
|
||||
public async Task<CrBrowseSeriesBase?> GetSeasonalSeries(string season, string year, string? crLocale){
|
||||
NameValueCollection query = HttpUtility.ParseQueryString(new UriBuilder().Query);
|
||||
|
||||
if (!string.IsNullOrEmpty(crLocale)){
|
||||
query["locale"] = crLocale;
|
||||
}
|
||||
|
||||
query["seasonal_tag"] = season.ToLower() + "-" + year;
|
||||
query["n"] = "100";
|
||||
|
||||
var request = HttpClientReq.CreateRequestMessage($"{ApiUrls.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);
|
||||
|
||||
return series;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ public class CrunchyrollManager{
|
|||
options.CustomCalendar = true;
|
||||
options.DlVideoOnce = true;
|
||||
options.StreamEndpoint = "web/firefox";
|
||||
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
||||
options.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
||||
options.HistoryLang = DefaultLocale;
|
||||
|
||||
options.BackgroundImageOpacity = 0.5;
|
||||
|
|
@ -177,7 +177,7 @@ public class CrunchyrollManager{
|
|||
optionsYaml.CustomCalendar = true;
|
||||
optionsYaml.DlVideoOnce = true;
|
||||
optionsYaml.StreamEndpoint = "web/firefox";
|
||||
optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.ScaledBorderAndShadowYes;
|
||||
optionsYaml.SubsAddScaledBorder = ScaledBorderAndShadowSelection.DontAdd;
|
||||
optionsYaml.HistoryLang = DefaultLocale;
|
||||
|
||||
optionsYaml.BackgroundImageOpacity = 0.5;
|
||||
|
|
@ -230,6 +230,29 @@ public class CrunchyrollManager{
|
|||
}
|
||||
}
|
||||
|
||||
public static async Task<string> GetBase64EncodedTokenAsync(){
|
||||
string url = "https://static.crunchyroll.com/vilos-v2/web/vilos/js/bundle.js";
|
||||
|
||||
try{
|
||||
string jsContent = await HttpClientReq.Instance.GetHttpClient().GetStringAsync(url);
|
||||
|
||||
Match match = Regex.Match(jsContent, @"prod=""([\w-]+:[\w-]+)""");
|
||||
|
||||
if (!match.Success)
|
||||
throw new Exception("Token not found in JS file.");
|
||||
|
||||
string token = match.Groups[1].Value;
|
||||
|
||||
byte[] tokenBytes = Encoding.UTF8.GetBytes(token);
|
||||
string base64Token = Convert.ToBase64String(tokenBytes);
|
||||
|
||||
return base64Token;
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"Auth Token Fetch Error: {ex.Message}");
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
public async Task Init(){
|
||||
if (CrunOptions.LogMode){
|
||||
CfgManager.EnableLogMode();
|
||||
|
|
@ -237,6 +260,12 @@ public class CrunchyrollManager{
|
|||
CfgManager.DisableLogMode();
|
||||
}
|
||||
|
||||
var token = await GetBase64EncodedTokenAsync();
|
||||
|
||||
if (!string.IsNullOrEmpty(token)){
|
||||
ApiUrls.authBasicMob = "Basic " + token;
|
||||
}
|
||||
|
||||
var jsonFiles = Directory.Exists(CfgManager.PathENCODING_PRESETS_DIR) ? Directory.GetFiles(CfgManager.PathENCODING_PRESETS_DIR, "*.json") :[];
|
||||
|
||||
foreach (var file in jsonFiles){
|
||||
|
|
@ -331,6 +360,7 @@ public class CrunchyrollManager{
|
|||
|
||||
if (options.SkipMuxing == false){
|
||||
bool syncError = false;
|
||||
bool muxError = false;
|
||||
|
||||
data.DownloadProgress = new DownloadProgress(){
|
||||
IsDownloading = true,
|
||||
|
|
@ -342,6 +372,10 @@ public class CrunchyrollManager{
|
|||
|
||||
QueueManager.Instance.Queue.Refresh();
|
||||
|
||||
if (options.MuxFonts){
|
||||
await FontsManager.Instance.GetFontsAsync();
|
||||
}
|
||||
|
||||
var fileNameAndPath = options.DownloadToTempFolder
|
||||
? Path.Combine(res.TempFolderPath ?? string.Empty, res.FileName ?? string.Empty)
|
||||
: Path.Combine(res.FolderPath ?? string.Empty, res.FileName ?? string.Empty);
|
||||
|
|
@ -357,6 +391,7 @@ public class CrunchyrollManager{
|
|||
SkipSubMux = options.SkipSubsMux,
|
||||
Output = fileNameAndPath,
|
||||
Mp4 = options.Mp4,
|
||||
MuxFonts = options.MuxFonts,
|
||||
VideoTitle = res.VideoTitle,
|
||||
Novids = options.Novids,
|
||||
NoCleanup = options.Nocleanup,
|
||||
|
|
@ -380,6 +415,10 @@ public class CrunchyrollManager{
|
|||
mergers.Add(result.merger);
|
||||
}
|
||||
|
||||
if (!result.isMuxed){
|
||||
muxError = true;
|
||||
}
|
||||
|
||||
if (result.syncError){
|
||||
syncError = true;
|
||||
}
|
||||
|
|
@ -417,6 +456,7 @@ public class CrunchyrollManager{
|
|||
SkipSubMux = options.SkipSubsMux,
|
||||
Output = fileNameAndPath,
|
||||
Mp4 = options.Mp4,
|
||||
MuxFonts = options.MuxFonts,
|
||||
VideoTitle = res.VideoTitle,
|
||||
Novids = options.Novids,
|
||||
NoCleanup = options.Nocleanup,
|
||||
|
|
@ -437,12 +477,13 @@ public class CrunchyrollManager{
|
|||
fileNameAndPath);
|
||||
|
||||
syncError = result.syncError;
|
||||
muxError = !result.isMuxed;
|
||||
|
||||
if (result is{ merger: not null, isMuxed: true }){
|
||||
result.merger.CleanUp();
|
||||
}
|
||||
|
||||
if (options.IsEncodeEnabled){
|
||||
if (options.IsEncodeEnabled && !muxError){
|
||||
data.DownloadProgress = new DownloadProgress(){
|
||||
IsDownloading = true,
|
||||
Percent = 100,
|
||||
|
|
@ -469,10 +510,10 @@ public class CrunchyrollManager{
|
|||
Percent = 100,
|
||||
Time = 0,
|
||||
DownloadSpeed = 0,
|
||||
Doing = "Done" + (syncError ? " - Couldn't sync dubs" : "")
|
||||
Doing = (muxError ? "Muxing Failed" : "Done") + (syncError ? " - Couldn't sync dubs" : "")
|
||||
};
|
||||
|
||||
if (options.RemoveFinishedDownload && !syncError){
|
||||
if (CrunOptions.RemoveFinishedDownload && !syncError){
|
||||
QueueManager.Instance.Queue.Remove(data);
|
||||
}
|
||||
} else{
|
||||
|
|
@ -498,7 +539,7 @@ public class CrunchyrollManager{
|
|||
Doing = "Done - Skipped muxing"
|
||||
};
|
||||
|
||||
if (options.RemoveFinishedDownload){
|
||||
if (CrunOptions.RemoveFinishedDownload){
|
||||
QueueManager.Instance.Queue.Remove(data);
|
||||
}
|
||||
}
|
||||
|
|
@ -515,6 +556,18 @@ public class CrunchyrollManager{
|
|||
_ = CrEpisode.MarkAsWatched(data.Data.First().MediaId);
|
||||
}
|
||||
|
||||
if (QueueManager.Instance.Queue.Count == 0){
|
||||
try{
|
||||
var audioPath = CrunOptions.DownloadFinishedSoundPath;
|
||||
if (!string.IsNullOrEmpty(audioPath)){
|
||||
var player = new AudioPlayer();
|
||||
player.Play(audioPath);
|
||||
}
|
||||
} catch (Exception exception){
|
||||
Console.Error.WriteLine("Failed to play sound: " + exception);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
|
@ -630,7 +683,7 @@ public class CrunchyrollManager{
|
|||
|
||||
bool muxDesc = false;
|
||||
if (options.MuxDescription){
|
||||
var descriptionPath = data.Where(a => a.Type == DownloadMediaType.Description).First().Path;
|
||||
var descriptionPath = data.First(a => a.Type == DownloadMediaType.Description).Path;
|
||||
if (File.Exists(descriptionPath)){
|
||||
muxDesc = true;
|
||||
} else{
|
||||
|
|
@ -649,7 +702,7 @@ public class CrunchyrollManager{
|
|||
Subtitles = data.Where(a => a.Type == DownloadMediaType.Subtitle).Select(a => new SubtitleInput
|
||||
{ File = a.Path ?? string.Empty, Language = a.Language, ClosedCaption = a.Cc ?? false, Signs = a.Signs ?? false, RelatedVideoDownloadMedia = a.RelatedVideoDownloadMedia }).ToList(),
|
||||
KeepAllVideos = options.KeepAllVideos,
|
||||
Fonts = FontsManager.Instance.MakeFontsList(CfgManager.PathFONTS_DIR, subsList),
|
||||
Fonts = options.MuxFonts ? 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(){
|
||||
|
|
@ -714,11 +767,9 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
if (!options.Mp4 && !muxToMp3){
|
||||
await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE);
|
||||
isMuxed = true;
|
||||
isMuxed = await merger.Merge("mkvmerge", CfgManager.PathMKVMERGE);
|
||||
} else{
|
||||
await merger.Merge("ffmpeg", CfgManager.PathFFMPEG);
|
||||
isMuxed = true;
|
||||
isMuxed = await merger.Merge("ffmpeg", CfgManager.PathFFMPEG);
|
||||
}
|
||||
|
||||
return (merger, isMuxed, syncError);
|
||||
|
|
@ -916,21 +967,43 @@ public class CrunchyrollManager{
|
|||
var fetchPlaybackData = await FetchPlaybackData(options, mediaId, mediaGuid, data.Music);
|
||||
|
||||
if (!fetchPlaybackData.IsOk){
|
||||
if (!fetchPlaybackData.IsOk && fetchPlaybackData.error != string.Empty){
|
||||
var s = fetchPlaybackData.error;
|
||||
var error = StreamError.FromJson(s);
|
||||
if (error != null && error.IsTooManyActiveStreamsError()){
|
||||
var errorJson = fetchPlaybackData.error;
|
||||
if (!string.IsNullOrEmpty(errorJson)){
|
||||
var error = StreamError.FromJson(errorJson);
|
||||
|
||||
if (error?.IsTooManyActiveStreamsError() == true){
|
||||
MainWindow.Instance.ShowError("Too many active streams that couldn't be stopped");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Too many active streams that couldn't be stopped\nClose open cruchyroll tabs in your browser"
|
||||
ErrorText = "Too many active streams that couldn't be stopped\nClose open Crunchyroll tabs in your browser"
|
||||
};
|
||||
}
|
||||
|
||||
if (error?.Error.Contains("Account maturity rating is lower than video rating") == true ||
|
||||
errorJson.Contains("Account maturity rating is lower than video rating")){
|
||||
MainWindow.Instance.ShowError("Account maturity rating is lower than video rating\nChange it in the Crunchyroll account settings");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Account maturity rating is lower than video rating"
|
||||
};
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(error?.Error)){
|
||||
MainWindow.Instance.ShowError($"Couldn't get Playback Data\n{error.Error}");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
FileName = "./unknown",
|
||||
ErrorText = "Playback data not found"
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and crunchyroll");
|
||||
MainWindow.Instance.ShowError("Couldn't get Playback Data\nTry again later or else check logs and Crunchyroll");
|
||||
return new DownloadResponse{
|
||||
Data = new List<DownloadedMedia>(),
|
||||
Error = true,
|
||||
|
|
@ -939,6 +1012,7 @@ public class CrunchyrollManager{
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
var pbData = fetchPlaybackData.pbData;
|
||||
|
||||
List<string> hsLangs = new List<string>();
|
||||
|
|
@ -1815,7 +1889,11 @@ public class CrunchyrollManager{
|
|||
}
|
||||
|
||||
if (data.DownloadSubs.Contains("all") || data.DownloadSubs.Contains(langItem.CrLocale)){
|
||||
var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url ?? string.Empty, HttpMethod.Get, false, false, null);
|
||||
if (string.IsNullOrEmpty(subsItem.url)){
|
||||
continue;
|
||||
}
|
||||
|
||||
var subsAssReq = HttpClientReq.CreateRequestMessage(subsItem.url, HttpMethod.Get, false, false, null);
|
||||
|
||||
var subsAssReqResponse = await HttpClientReq.Instance.SendHttpRequest(subsAssReq);
|
||||
|
||||
|
|
@ -2025,7 +2103,7 @@ public class CrunchyrollManager{
|
|||
Data = new Dictionary<string, StreamDetails>()
|
||||
};
|
||||
|
||||
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
|
||||
var playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/{options.StreamEndpoint}/play";
|
||||
var playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
||||
|
||||
if (!playbackRequestResponse.IsOk){
|
||||
|
|
@ -2036,7 +2114,7 @@ public class CrunchyrollManager{
|
|||
temppbData = await ProcessPlaybackResponseAsync(playbackRequestResponse.ResponseContent, mediaId, mediaGuidId);
|
||||
} else{
|
||||
Console.WriteLine("Request Stream URLs FAILED! Attempting fallback");
|
||||
playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v1/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
|
||||
playbackEndpoint = $"https://cr-play-service.prd.crunchyrollsvc.com/v2/{(music ? "music/" : "")}{mediaGuidId}/web/firefox/play";
|
||||
playbackRequestResponse = await SendPlaybackRequestAsync(playbackEndpoint);
|
||||
|
||||
if (!playbackRequestResponse.IsOk){
|
||||
|
|
@ -2185,13 +2263,11 @@ public class CrunchyrollManager{
|
|||
foreach (CrunchyChapter chapter in chapterData.Chapters){
|
||||
if (chapter.start == null || chapter.end == null) continue;
|
||||
|
||||
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
TimeSpan startTime = TimeSpan.FromSeconds(chapter.start.Value);
|
||||
TimeSpan endTime = TimeSpan.FromSeconds(chapter.end.Value);
|
||||
|
||||
DateTime startTime = epoch.AddSeconds(chapter.start.Value);
|
||||
DateTime endTime = epoch.AddSeconds(chapter.end.Value);
|
||||
|
||||
string startFormatted = startTime.ToString("HH:mm:ss") + ".00";
|
||||
string endFormatted = endTime.ToString("HH:mm:ss") + ".00";
|
||||
string startFormatted = startTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
|
||||
string endFormatted = endTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
|
||||
|
||||
int chapterNumber = (compiledChapters.Count / 2) + 1;
|
||||
if (chapter.type == "intro"){
|
||||
|
|
@ -2227,19 +2303,12 @@ public class CrunchyrollManager{
|
|||
if (showRequestResponse.IsOk){
|
||||
CrunchyOldChapter chapterData = Helpers.Deserialize<CrunchyOldChapter>(showRequestResponse.ResponseContent, SettingsJsonSerializerSettings) ?? new CrunchyOldChapter();
|
||||
|
||||
DateTime epoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
|
||||
TimeSpan startTime = TimeSpan.FromSeconds(chapterData.startTime);
|
||||
TimeSpan endTime = TimeSpan.FromSeconds(chapterData.endTime);
|
||||
|
||||
DateTime startTime = epoch.AddSeconds(chapterData.startTime);
|
||||
DateTime endTime = epoch.AddSeconds(chapterData.endTime);
|
||||
string startFormatted = startTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
|
||||
string endFormatted = endTime.ToString(@"hh\:mm\:ss\.fff", CultureInfo.InvariantCulture);
|
||||
|
||||
string[] startTimeParts = startTime.ToString(CultureInfo.CurrentCulture).Split('.');
|
||||
string[] endTimeParts = endTime.ToString(CultureInfo.CurrentCulture).Split('.');
|
||||
|
||||
string startMs = startTimeParts.Length > 1 ? startTimeParts[1] : "00";
|
||||
string endMs = endTimeParts.Length > 1 ? endTimeParts[1] : "00";
|
||||
|
||||
string startFormatted = startTime.ToString("HH:mm:ss") + "." + startMs;
|
||||
string endFormatted = endTime.ToString("HH:mm:ss") + "." + endMs;
|
||||
|
||||
int chapterNumber = (compiledChapters.Count / 2) + 1;
|
||||
if (chapterData.startTime > 1){
|
||||
|
|
|
|||
|
|
@ -58,6 +58,9 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _muxToMp4;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _muxFonts;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _syncTimings;
|
||||
|
|
@ -310,6 +313,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
KeepDubsSeparate = options.KeepDubsSeperate;
|
||||
DownloadChapters = options.Chapters;
|
||||
MuxToMp4 = options.Mp4;
|
||||
MuxFonts = options.MuxFonts;
|
||||
SyncTimings = options.SyncTiming;
|
||||
SkipSubMux = options.SkipSubsMux;
|
||||
LeadingNumbers = options.Numbers;
|
||||
|
|
@ -375,6 +379,7 @@ public partial class CrunchyrollSettingsViewModel : ViewModelBase{
|
|||
CrunchyrollManager.Instance.CrunOptions.Chapters = DownloadChapters;
|
||||
CrunchyrollManager.Instance.CrunOptions.SkipMuxing = SkipMuxing;
|
||||
CrunchyrollManager.Instance.CrunOptions.Mp4 = MuxToMp4;
|
||||
CrunchyrollManager.Instance.CrunOptions.MuxFonts = MuxFonts;
|
||||
CrunchyrollManager.Instance.CrunOptions.SyncTiming = SyncTimings;
|
||||
CrunchyrollManager.Instance.CrunOptions.SkipSubsMux = SkipSubMux;
|
||||
CrunchyrollManager.Instance.CrunOptions.Numbers = Math.Clamp((int)(LeadingNumbers ?? 0), 0, 10);
|
||||
|
|
|
|||
|
|
@ -377,6 +377,12 @@
|
|||
<CheckBox IsChecked="{Binding DefaultSubSigns}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="Include Fonts" Description="Includes the fonts in the mkv">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding MuxFonts}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
<controls:SettingsExpanderItem IsVisible="{Binding !SkipMuxing}" Content="File title"
|
||||
Description="${seriesTitle} ${seasonTitle} ${title} ${season} ${episode} ${height} ${width} ${dubs}">
|
||||
|
|
|
|||
|
|
@ -49,9 +49,12 @@ public partial class ProgramManager : ObservableObject{
|
|||
[ObservableProperty]
|
||||
private bool _updateAvailable = true;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _opacityButton = 0.4;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _finishedLoading;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _navigationLock;
|
||||
|
||||
|
|
@ -122,6 +125,8 @@ public partial class ProgramManager : ObservableObject{
|
|||
|
||||
UpdateAvailable = await Updater.Instance.CheckForUpdatesAsync();
|
||||
|
||||
OpacityButton = UpdateAvailable ? 1.0 : 0.4;
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
|
||||
if (_faTheme != null) _faTheme.CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
|
||||
}
|
||||
|
|
@ -176,9 +181,8 @@ public partial class ProgramManager : ObservableObject{
|
|||
|
||||
|
||||
private void CleanUpOldUpdater(){
|
||||
|
||||
var executableExtension = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? ".exe" : string.Empty;
|
||||
|
||||
|
||||
string backupFilePath = Path.Combine(Directory.GetCurrentDirectory(), $"Updater{executableExtension}.bak");
|
||||
|
||||
if (File.Exists(backupFilePath)){
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||
using System.Collections.Specialized;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.CustomList;
|
||||
|
|
@ -16,7 +17,7 @@ using ReactiveUI;
|
|||
|
||||
namespace CRD.Downloader;
|
||||
|
||||
public class QueueManager{
|
||||
public partial class QueueManager : ObservableObject{
|
||||
#region Download Variables
|
||||
|
||||
public RefreshableObservableCollection<CrunchyEpMeta> Queue = new RefreshableObservableCollection<CrunchyEpMeta>();
|
||||
|
|
@ -24,7 +25,9 @@ public class QueueManager{
|
|||
public int ActiveDownloads;
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _hasFailedItem;
|
||||
|
||||
#region Singelton
|
||||
|
||||
|
|
@ -87,8 +90,11 @@ public class QueueManager{
|
|||
downloadItem.StartDownload();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HasFailedItem = Queue.Any(item => item.DownloadProgress.Error);
|
||||
|
||||
}
|
||||
|
||||
|
||||
public async Task CrAddEpisodeToQueue(string epId, string crLocale, List<string> dubLang, bool updateHistory = false, bool onlySubs = false){
|
||||
if (string.IsNullOrEmpty(epId)){
|
||||
|
|
@ -230,7 +236,9 @@ public class QueueManager{
|
|||
newOptions.DubLang = dubLang;
|
||||
|
||||
movieMeta.DownloadSettings = newOptions;
|
||||
|
||||
|
||||
movieMeta.VideoQuality = CrunchyrollManager.Instance.CrunOptions.QualityVideo;
|
||||
|
||||
Queue.Add(movieMeta);
|
||||
|
||||
Console.WriteLine("Added Movie to Queue");
|
||||
|
|
|
|||
29
CRD/Utils/AudioPlayer.cs
Normal file
29
CRD/Utils/AudioPlayer.cs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
using System;
|
||||
using NetCoreAudio;
|
||||
|
||||
namespace CRD.Utils;
|
||||
|
||||
public class AudioPlayer{
|
||||
private readonly Player _player;
|
||||
private bool _isPlaying = false;
|
||||
|
||||
public AudioPlayer(){
|
||||
_player = new Player();
|
||||
}
|
||||
|
||||
public async void Play(string path){
|
||||
if (_isPlaying){
|
||||
Console.WriteLine("Audio is already playing, ignoring duplicate request.");
|
||||
return;
|
||||
}
|
||||
|
||||
_isPlaying = true;
|
||||
await _player.Play(path);
|
||||
_isPlaying = false;
|
||||
}
|
||||
|
||||
public async void Stop(){
|
||||
await _player.Stop();
|
||||
_isPlaying = false;
|
||||
}
|
||||
}
|
||||
|
|
@ -39,41 +39,57 @@ public class Widevine{
|
|||
public Widevine(){
|
||||
try{
|
||||
if (Directory.Exists(CfgManager.PathWIDEVINE_DIR)){
|
||||
var files = Directory.GetFiles(CfgManager.PathWIDEVINE_DIR);
|
||||
|
||||
foreach (var file in files){
|
||||
foreach (var file in Directory.EnumerateFiles(CfgManager.PathWIDEVINE_DIR)){
|
||||
var fileInfo = new FileInfo(file);
|
||||
if (fileInfo.Length < 1024 * 8 && !fileInfo.Attributes.HasFlag(FileAttributes.Directory)){
|
||||
string fileContents = File.ReadAllText(file, Encoding.UTF8);
|
||||
if (fileContents.Contains("-BEGIN RSA PRIVATE KEY-") || fileContents.Contains("-BEGIN PRIVATE KEY-")){
|
||||
privateKey = File.ReadAllBytes(file);
|
||||
}
|
||||
|
||||
if (fileInfo.Length >= 1024 * 8 || fileInfo.Attributes.HasFlag(FileAttributes.Directory))
|
||||
continue;
|
||||
|
||||
string fileContents = File.ReadAllText(file, Encoding.UTF8);
|
||||
|
||||
if (fileContents.Contains("widevine_cdm_version")){
|
||||
identifierBlob = File.ReadAllBytes(file);
|
||||
}
|
||||
if (IsPrivateKey(fileContents)){
|
||||
privateKey = File.ReadAllBytes(file);
|
||||
} else if (IsWidevineIdentifierBlob(fileContents)){
|
||||
identifierBlob = File.ReadAllBytes(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (privateKey.Length != 0 && identifierBlob.Length != 0){
|
||||
if (privateKey?.Length > 0 && identifierBlob?.Length > 0){
|
||||
canDecrypt = true;
|
||||
} else if (privateKey.Length == 0){
|
||||
Console.Error.WriteLine("Private key missing");
|
||||
canDecrypt = false;
|
||||
} else if (identifierBlob.Length == 0){
|
||||
Console.Error.WriteLine("Identifier blob missing");
|
||||
} else{
|
||||
canDecrypt = false;
|
||||
if (privateKey == null || privateKey.Length == 0){
|
||||
Console.Error.WriteLine("Private key missing");
|
||||
}
|
||||
|
||||
if (identifierBlob == null || identifierBlob.Length == 0){
|
||||
Console.Error.WriteLine("Identifier blob missing");
|
||||
}
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine("Widevine: " + e);
|
||||
} catch (IOException ioEx){
|
||||
Console.Error.WriteLine("I/O error accessing Widevine files: " + ioEx);
|
||||
canDecrypt = false;
|
||||
} catch (UnauthorizedAccessException uaEx){
|
||||
Console.Error.WriteLine("Permission error accessing Widevine files: " + uaEx);
|
||||
canDecrypt = false;
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine("Unexpected Widevine error: " + ex);
|
||||
canDecrypt = false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"CDM available: {canDecrypt}");
|
||||
}
|
||||
|
||||
private bool IsPrivateKey(string content){
|
||||
return content.Contains("-BEGIN RSA PRIVATE KEY-", StringComparison.Ordinal) ||
|
||||
content.Contains("-BEGIN PRIVATE KEY-", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private bool IsWidevineIdentifierBlob(string content){
|
||||
return content.Contains("widevine_cdm_version", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
public async Task<List<ContentKey>> getKeys(string? pssh, string licenseServer, Dictionary<string, string> authData){
|
||||
if (pssh == null || !canDecrypt){
|
||||
Console.Error.WriteLine("Missing pssh or cdm files");
|
||||
|
|
|
|||
|
|
@ -15,36 +15,36 @@ using YamlDotNet.Serialization.NamingConventions;
|
|||
namespace CRD.Utils.Files;
|
||||
|
||||
public class CfgManager{
|
||||
private static string WorkingDirectory = AppContext.BaseDirectory;
|
||||
private static string workingDirectory = AppContext.BaseDirectory;
|
||||
|
||||
public static readonly string PathCrTokenOld = Path.Combine(WorkingDirectory, "config", "cr_token.yml");
|
||||
public static readonly string PathCrDownloadOptionsOld = Path.Combine(WorkingDirectory, "config", "settings.yml");
|
||||
public static readonly string PathCrTokenOld = Path.Combine(workingDirectory, "config", "cr_token.yml");
|
||||
public static readonly string PathCrDownloadOptionsOld = Path.Combine(workingDirectory, "config", "settings.yml");
|
||||
|
||||
public static readonly string PathCrToken = Path.Combine(WorkingDirectory, "config", "cr_token.json");
|
||||
public static readonly string PathCrDownloadOptions = Path.Combine(WorkingDirectory, "config", "settings.json");
|
||||
public static readonly string PathCrToken = Path.Combine(workingDirectory, "config", "cr_token.json");
|
||||
public static readonly string PathCrDownloadOptions = Path.Combine(workingDirectory, "config", "settings.json");
|
||||
|
||||
public static readonly string PathCrHistory = Path.Combine(WorkingDirectory, "config", "history.json");
|
||||
public static readonly string PathWindowSettings = Path.Combine(WorkingDirectory, "config", "windowSettings.json");
|
||||
public static readonly string PathCrHistory = Path.Combine(workingDirectory, "config", "history.json");
|
||||
public static readonly string PathWindowSettings = Path.Combine(workingDirectory, "config", "windowSettings.json");
|
||||
|
||||
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 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 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 PathShakaPackager = Path.Combine(WorkingDirectory, "lib", "shaka-packager" + ExecutableExtension);
|
||||
public static readonly string PathMP4Decrypt = Path.Combine(workingDirectory, "lib", "mp4decrypt" + ExecutableExtension);
|
||||
public static readonly string PathShakaPackager = Path.Combine(workingDirectory, "lib", "shaka-packager" + ExecutableExtension);
|
||||
|
||||
public static readonly string PathWIDEVINE_DIR = Path.Combine(WorkingDirectory, "widevine");
|
||||
public static readonly string PathWIDEVINE_DIR = Path.Combine(workingDirectory, "widevine");
|
||||
|
||||
public static readonly string PathVIDEOS_DIR = Path.Combine(WorkingDirectory, "video");
|
||||
public static readonly string 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, "fonts");
|
||||
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, "fonts");
|
||||
|
||||
public static readonly string PathLogFile = Path.Combine(WorkingDirectory, "logfile.txt");
|
||||
public static readonly string PathLogFile = Path.Combine(workingDirectory, "logfile.txt");
|
||||
|
||||
private static StreamWriter logFile;
|
||||
private static bool isLogModeEnabled = false;
|
||||
|
|
@ -290,7 +290,7 @@ public class CfgManager{
|
|||
}
|
||||
|
||||
lock (fileLock){
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write))
|
||||
using (var fileStream = new FileStream(pathToFile, FileMode.Create, FileAccess.Write, FileShare.None))
|
||||
using (var streamWriter = new StreamWriter(fileStream))
|
||||
using (var jsonWriter = new JsonTextWriter(streamWriter){ Formatting = Formatting.Indented }){
|
||||
var serializer = new JsonSerializer();
|
||||
|
|
|
|||
|
|
@ -453,6 +453,8 @@ public class HlsDownloader{
|
|||
Console.WriteLine($"\tError: {ex.Message}");
|
||||
if (attempt == retryCount)
|
||||
throw; // rethrow after last retry
|
||||
|
||||
await Task.Delay(_data.WaitTime);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -237,7 +237,11 @@ public class Helpers{
|
|||
|
||||
process.OutputDataReceived += (sender, e) => {
|
||||
if (!string.IsNullOrEmpty(e.Data)){
|
||||
Console.WriteLine(e.Data);
|
||||
if (e.Data.StartsWith("Error:")){
|
||||
Console.Error.WriteLine(e.Data);
|
||||
} else{
|
||||
Console.WriteLine(e.Data);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -216,6 +216,11 @@ public class HttpClientReq{
|
|||
|
||||
|
||||
public static HttpRequestMessage CreateRequestMessage(string uri, HttpMethod requestMethod, bool authHeader, bool disableDrmHeader, NameValueCollection? query){
|
||||
if (string.IsNullOrEmpty(uri)){
|
||||
Console.Error.WriteLine($" Request URI is empty");
|
||||
return new HttpRequestMessage(HttpMethod.Get, "about:blank");
|
||||
}
|
||||
|
||||
UriBuilder uriBuilder = new UriBuilder(uri);
|
||||
|
||||
if (query != null){
|
||||
|
|
@ -263,12 +268,8 @@ public static class ApiUrls{
|
|||
public static readonly string BetaBrowse = ApiBeta + "/content/v1/browse";
|
||||
public static readonly string BetaCms = ApiBeta + "/cms/v2";
|
||||
public static readonly string DRM = ApiBeta + "/drm/v1/auth";
|
||||
|
||||
public static string authBasicMob = "Basic eHVuaWh2ZWRidDNtYmlzdWhldnQ6MWtJUzVkeVR2akUwX3JxYUEzWWVBaDBiVVhVbXhXMTE=";
|
||||
|
||||
public static readonly string authBasic = "Basic bm9haWhkZXZtXzZpeWcwYThsMHE6";
|
||||
|
||||
public static readonly string authBasicMob = "Basic ZG1yeWZlc2NkYm90dWJldW56NXo6NU45aThPV2cyVmtNcm1oekNfNUNXekRLOG55SXo0QU0=";
|
||||
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";
|
||||
public static readonly string MobileUserAgent = "Crunchyroll/3.74.2 Android/14 okhttp/4.12.0";
|
||||
public static readonly string MobileUserAgent = "Crunchyroll/3.78.3 Android/15 okhttp/4.12.0";
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ using System.Text.RegularExpressions;
|
|||
using System.Threading.Tasks;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Views;
|
||||
|
||||
namespace CRD.Utils.Muxing;
|
||||
|
||||
|
|
@ -65,7 +66,7 @@ public class FontsManager{
|
|||
var fontLoc = Path.Combine(CfgManager.PathFONTS_DIR, font);
|
||||
|
||||
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length != 0){
|
||||
Console.WriteLine($"{font} already downloaded!");
|
||||
// Console.WriteLine($"{font} already downloaded!");
|
||||
} else{
|
||||
var fontFolder = Path.GetDirectoryName(fontLoc);
|
||||
if (File.Exists(fontLoc) && new FileInfo(fontLoc).Length == 0){
|
||||
|
|
@ -82,19 +83,18 @@ public class FontsManager{
|
|||
|
||||
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}");
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -171,6 +171,8 @@ public class FontsManager{
|
|||
Console.WriteLine((isNstr ? "\n" : "") + "Required fonts: {0} (Total: {1})", string.Join(", ", fontsNameList), fontsNameList.Count);
|
||||
}
|
||||
|
||||
List<string> missingFonts = new List<string>();
|
||||
|
||||
foreach (var f in fontsNameList){
|
||||
if (Fonts.TryGetValue(f.Key, out var fontFiles)){
|
||||
foreach (var fontFile in fontFiles){
|
||||
|
|
@ -180,9 +182,15 @@ public class FontsManager{
|
|||
fontsList.Add(new ParsedFont{ Name = fontFile, Path = fontPath, Mime = mime });
|
||||
}
|
||||
}
|
||||
} else{
|
||||
missingFonts.Add(f.Key);
|
||||
}
|
||||
}
|
||||
|
||||
if (missingFonts.Count > 0){
|
||||
MainWindow.Instance.ShowError($"Missing Fonts: \n{string.Join(", ", fontsNameList)}");
|
||||
}
|
||||
|
||||
return fontsList;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -385,7 +385,7 @@ public class Merger{
|
|||
}
|
||||
|
||||
|
||||
public async Task Merge(string type, string bin){
|
||||
public async Task<bool> Merge(string type, string bin){
|
||||
string command = type switch{
|
||||
"ffmpeg" => FFmpeg(),
|
||||
"mkvmerge" => MkvMerge(),
|
||||
|
|
@ -394,7 +394,7 @@ public class Merger{
|
|||
|
||||
if (string.IsNullOrEmpty(command)){
|
||||
Console.Error.WriteLine("Unable to merge files.");
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[{type}] Started merging");
|
||||
|
|
@ -404,9 +404,12 @@ public class Merger{
|
|||
Console.WriteLine($"[{type}] Mkvmerge finished with at least one warning");
|
||||
} else if (!result.IsOk){
|
||||
Console.Error.WriteLine($"[{type}] Merging failed with exit code {result.ErrorCode}");
|
||||
return false;
|
||||
} else{
|
||||
Console.WriteLine($"[{type} Done]");
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -459,6 +462,7 @@ public class CrunchyMuxOptions{
|
|||
public bool? KeepAllVideos{ get; set; }
|
||||
public bool? Novids{ get; set; }
|
||||
public bool Mp4{ get; set; }
|
||||
public bool MuxFonts{ get; set; }
|
||||
public bool MuxDescription{ get; set; }
|
||||
public string ForceMuxer{ get; set; }
|
||||
public bool? NoCleanup{ get; set; }
|
||||
|
|
|
|||
|
|
@ -64,6 +64,16 @@ public partial class AnilistSeries : ObservableObject{
|
|||
[JsonIgnore]
|
||||
[ObservableProperty]
|
||||
public bool _isInHistory;
|
||||
|
||||
[ObservableProperty]
|
||||
public bool _isExpanded;
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> AudioLocales{ get; set; } =[];
|
||||
|
||||
[JsonIgnore]
|
||||
public List<string> SubtitleLocales{ get; set; } =[];
|
||||
|
||||
}
|
||||
|
||||
public class Title{
|
||||
|
|
|
|||
|
|
@ -47,22 +47,23 @@ public partial class CalendarEpisode : INotifyPropertyChanged{
|
|||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
[RelayCommand]
|
||||
public void AddEpisodeToQue(){
|
||||
if (CalendarEpisodes.Count > 0){
|
||||
foreach (var calendarEpisode in CalendarEpisodes){
|
||||
calendarEpisode.AddEpisodeToQue();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task AddEpisodeToQue(){
|
||||
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);
|
||||
await QueueManager.Instance.CrAddEpisodeToQueue(id, Languages.Locale2language(locale).CrLocale, CrunchyrollManager.Instance.CrunOptions.DubLang, true);
|
||||
}
|
||||
}
|
||||
|
||||
if (CalendarEpisodes.Count > 0){
|
||||
foreach (var calendarEpisode in CalendarEpisodes){
|
||||
calendarEpisode.AddEpisodeToQue();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public async Task LoadImage(int width = 0, int height = 0){
|
||||
|
|
|
|||
|
|
@ -36,6 +36,13 @@ public class CrDownloadOptions{
|
|||
[JsonProperty("background_image_path")]
|
||||
public string? BackgroundImagePath{ get; set; }
|
||||
|
||||
[JsonProperty("download_finished_play_sound")]
|
||||
public bool DownloadFinishedPlaySound{ get; set; }
|
||||
|
||||
[JsonProperty("download_finished_sound_path")]
|
||||
public string? DownloadFinishedSoundPath{ get; set; }
|
||||
|
||||
|
||||
[JsonProperty("background_image_opacity")]
|
||||
public double BackgroundImageOpacity{ get; set; }
|
||||
|
||||
|
|
@ -180,6 +187,9 @@ public class CrDownloadOptions{
|
|||
|
||||
[JsonProperty("mux_mp4")]
|
||||
public bool Mp4{ get; set; }
|
||||
|
||||
[JsonProperty("mux_fonts")]
|
||||
public bool MuxFonts{ get; set; }
|
||||
|
||||
[JsonProperty("mux_video_title")]
|
||||
public string? VideoTitle{ get; set; }
|
||||
|
|
@ -369,8 +379,7 @@ public class CrDownloadOptionsYaml{
|
|||
public string? ProxyPassword{ get; set; }
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
|
||||
#region Crunchyroll Settings
|
||||
|
||||
[YamlIgnore]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRD.Utils.Structs.History;
|
||||
using CRD.Views;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
|
|
@ -73,7 +74,7 @@ public class DownloadResponse{
|
|||
|
||||
public string? FolderPath{ get; set; }
|
||||
public string? TempFolderPath{ get; set; }
|
||||
|
||||
|
||||
public string VideoTitle{ get; set; }
|
||||
public bool Error{ get; set; }
|
||||
public string ErrorText{ get; set; }
|
||||
|
|
@ -107,14 +108,13 @@ public class StringItem{
|
|||
public string stringValue{ get; set; }
|
||||
}
|
||||
|
||||
public class WindowSettings
|
||||
{
|
||||
public double Width { get; set; }
|
||||
public double Height { get; set; }
|
||||
public int ScreenIndex { get; set; }
|
||||
public int PosX { get; set; }
|
||||
public int PosY { get; set; }
|
||||
public bool IsMaximized { get; set; }
|
||||
public class WindowSettings{
|
||||
public double Width{ get; set; }
|
||||
public double Height{ get; set; }
|
||||
public int ScreenIndex{ get; set; }
|
||||
public int PosX{ get; set; }
|
||||
public int PosY{ get; set; }
|
||||
public bool IsMaximized{ get; set; }
|
||||
}
|
||||
|
||||
public class ToastMessage(string message, ToastType type, int i){
|
||||
|
|
@ -143,4 +143,14 @@ public partial class SeasonViewModel : ObservableObject{
|
|||
public int Year{ get; set; }
|
||||
|
||||
public string Display => $"{Season}\n{Year}";
|
||||
}
|
||||
|
||||
public class SeasonDialogArgs{
|
||||
public HistorySeries? Series{ get; set; }
|
||||
public HistorySeason? Season{ get; set; }
|
||||
|
||||
public SeasonDialogArgs(HistorySeries? series, HistorySeason? season){
|
||||
Series = series;
|
||||
Season = season;
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ 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.Media.Imaging;
|
||||
|
|
@ -99,6 +100,12 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
[JsonIgnore]
|
||||
private bool _editModeEnabled;
|
||||
|
||||
[JsonIgnore]
|
||||
public string SeriesFolderPath{ get; set; }
|
||||
|
||||
[JsonIgnore]
|
||||
public bool SeriesFolderPathExists{ get; set; }
|
||||
|
||||
#region Settings Override
|
||||
|
||||
[JsonIgnore]
|
||||
|
|
@ -213,6 +220,9 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
|
||||
SelectedSubLang.CollectionChanged += Changes;
|
||||
SelectedDubLang.CollectionChanged += Changes;
|
||||
|
||||
UpdateSeriesFolderPath();
|
||||
|
||||
Loading = false;
|
||||
}
|
||||
|
||||
|
|
@ -516,4 +526,59 @@ public class HistorySeries : INotifyPropertyChanged{
|
|||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateSeriesFolderPath(){
|
||||
var season = Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath));
|
||||
|
||||
if (!string.IsNullOrEmpty(SeriesDownloadPath) && Directory.Exists(SeriesDownloadPath)){
|
||||
SeriesFolderPath = SeriesDownloadPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
|
||||
if (season is{ SeasonDownloadPath: not null }){
|
||||
try{
|
||||
var seasonPath = season.SeasonDownloadPath;
|
||||
var directoryInfo = new DirectoryInfo(seasonPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){
|
||||
string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty;
|
||||
|
||||
if (Directory.Exists(parentFolderPath)){
|
||||
SeriesFolderPath = parentFolderPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}");
|
||||
}
|
||||
} else{
|
||||
string customPath;
|
||||
|
||||
if (string.IsNullOrEmpty(SeriesTitle))
|
||||
return;
|
||||
|
||||
var seriesTitle = FileNameManager.CleanupFilename(SeriesTitle);
|
||||
|
||||
if (string.IsNullOrEmpty(seriesTitle))
|
||||
return;
|
||||
|
||||
// Check Crunchyroll download directory
|
||||
var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath;
|
||||
if (!string.IsNullOrEmpty(downloadDirPath)){
|
||||
customPath = Path.Combine(downloadDirPath, seriesTitle);
|
||||
} else{
|
||||
// Fallback to configured VIDEOS_DIR path
|
||||
customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
|
||||
}
|
||||
|
||||
// Check if custom path exists
|
||||
if (Directory.Exists(customPath)){
|
||||
SeriesFolderPath = customPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
}
|
||||
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SeriesFolderPathExists)));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -40,26 +40,29 @@ public class Languages{
|
|||
};
|
||||
|
||||
public static List<string> SortListByLangList(List<string> langList){
|
||||
var orderMap = languages.Select((value, index) => new { Value = value.CrLocale, Index = index })
|
||||
var orderMap = languages.Select((value, index) => new{ Value = value.CrLocale, Index = index })
|
||||
.ToDictionary(x => x.Value, x => x.Index);
|
||||
langList.Sort((x, y) =>
|
||||
{
|
||||
langList.Sort((x, y) => {
|
||||
bool xExists = orderMap.ContainsKey(x);
|
||||
bool yExists = orderMap.ContainsKey(y);
|
||||
|
||||
if (xExists && yExists)
|
||||
return orderMap[x].CompareTo(orderMap[y]); // Sort by main list order
|
||||
return orderMap[x].CompareTo(orderMap[y]); // Sort by main list order
|
||||
else if (xExists)
|
||||
return -1; // x comes before any missing value
|
||||
return -1; // x comes before any missing value
|
||||
else if (yExists)
|
||||
return 1; // y comes before any missing value
|
||||
return 1; // y comes before any missing value
|
||||
else
|
||||
return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values
|
||||
return string.CompareOrdinal(x, y); // Sort alphabetically or by another logic for missing values
|
||||
});
|
||||
|
||||
return langList;
|
||||
}
|
||||
|
||||
public static List<string> LocalListToLangList(List<Locale> langList){
|
||||
return SortListByLangList(langList.Select(seriesMetadataAudioLocale => seriesMetadataAudioLocale.GetEnumMemberValue()).ToList());
|
||||
}
|
||||
|
||||
public static LanguageItem FixAndFindCrLc(string cr_locale){
|
||||
if (string.IsNullOrEmpty(cr_locale)){
|
||||
return new LanguageItem();
|
||||
|
|
@ -69,14 +72,14 @@ public class Languages{
|
|||
return FindLang(str);
|
||||
}
|
||||
|
||||
public static string SubsFile(string fnOutput, string subsIndex, LanguageItem langItem, bool isCC, string ccTag , bool? isSigns = false, string? format = "ass", bool addIndexAndLangCode = true){
|
||||
public static string SubsFile(string fnOutput, string subsIndex, LanguageItem langItem, bool isCC, string ccTag, bool? isSigns = false, string? format = "ass", bool addIndexAndLangCode = true){
|
||||
subsIndex = (int.Parse(subsIndex) + 1).ToString().PadLeft(2, '0');
|
||||
string fileName = $"{fnOutput}";
|
||||
|
||||
if (addIndexAndLangCode){
|
||||
fileName += $".{subsIndex}.{langItem.CrLocale}";
|
||||
fileName += $".{langItem.CrLocale}"; //.{subsIndex}
|
||||
}
|
||||
|
||||
|
||||
//removed .{langItem.language} from file name at end
|
||||
|
||||
if (isCC){
|
||||
|
|
|
|||
19
CRD/Utils/UI/UiSeriesSeasonConverter.cs
Normal file
19
CRD/Utils/UI/UiSeriesSeasonConverter.cs
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using Avalonia.Data.Converters;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.History;
|
||||
|
||||
namespace CRD.Utils.UI;
|
||||
|
||||
public class UiSeriesSeasonConverter : IMultiValueConverter{
|
||||
public object Convert(IList<object> values, Type targetType, object parameter, CultureInfo culture){
|
||||
var series = values.Count > 0 && values[0] is HistorySeries hs ? hs : null;
|
||||
var season = values.Count > 1 && values[1] is HistorySeason hsn ? hsn : null;
|
||||
return new SeasonDialogArgs(series, season);
|
||||
}
|
||||
|
||||
public IList<object> ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
|
||||
=> throw new NotImplementedException();
|
||||
}
|
||||
|
|
@ -8,8 +8,10 @@ using System.Net.Http;
|
|||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CRD.Utils.Files;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace CRD.Utils.Updater;
|
||||
|
||||
|
|
@ -17,6 +19,8 @@ public class Updater : INotifyPropertyChanged{
|
|||
public double progress = 0;
|
||||
public bool failed = false;
|
||||
|
||||
public string latestVersion = "";
|
||||
|
||||
#region Singelton
|
||||
|
||||
private static Updater? _instance;
|
||||
|
|
@ -47,8 +51,10 @@ public class Updater : INotifyPropertyChanged{
|
|||
private string downloadUrl = "";
|
||||
private readonly string tempPath = Path.Combine(CfgManager.PathTEMP_DIR, "Update.zip");
|
||||
private readonly string extractPath = Path.Combine(CfgManager.PathTEMP_DIR, "ExtractedUpdate");
|
||||
private readonly string changelogFilePath = Path.Combine(AppContext.BaseDirectory, "CHANGELOG.md");
|
||||
|
||||
private readonly string apiEndpoint = "https://api.github.com/repos/Crunchy-DL/Crunchy-Downloader/releases/latest";
|
||||
private static readonly string apiEndpoint = "https://api.github.com/repos/Crunchy-DL/Crunchy-Downloader/releases";
|
||||
private static readonly string apiEndpointLatest = apiEndpoint + "/latest";
|
||||
|
||||
public async Task<bool> CheckForUpdatesAsync(){
|
||||
if (File.Exists(tempPath)){
|
||||
|
|
@ -86,19 +92,25 @@ public class Updater : INotifyPropertyChanged{
|
|||
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 response = await client.GetStringAsync(apiEndpointLatest);
|
||||
var releaseInfo = Helpers.Deserialize<GithubRelease>(response, null);
|
||||
|
||||
var latestVersion = releaseInfo.tag_name;
|
||||
|
||||
foreach (var asset in releaseInfo.assets){
|
||||
string assetName = (string)asset.name;
|
||||
if (assetName.Contains(platformName)){
|
||||
downloadUrl = asset.browser_download_url;
|
||||
break;
|
||||
}
|
||||
if (releaseInfo == null){
|
||||
Console.WriteLine($"Failed to get Update info");
|
||||
return false;
|
||||
}
|
||||
|
||||
latestVersion = releaseInfo.TagName;
|
||||
|
||||
if (releaseInfo.Assets != null)
|
||||
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;
|
||||
|
|
@ -109,10 +121,12 @@ public class Updater : INotifyPropertyChanged{
|
|||
|
||||
if (latestVersion != currentVersion){
|
||||
Console.WriteLine("Update available: " + latestVersion + " - Current Version: " + currentVersion);
|
||||
_ = UpdateChangelogAsync();
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.WriteLine("No updates available.");
|
||||
_ = UpdateChangelogAsync();
|
||||
return false;
|
||||
}
|
||||
} catch (Exception e){
|
||||
|
|
@ -121,6 +135,99 @@ public class Updater : INotifyPropertyChanged{
|
|||
}
|
||||
}
|
||||
|
||||
public async Task UpdateChangelogAsync(){
|
||||
var client = HttpClientReq.Instance.GetHttpClient();
|
||||
client.DefaultRequestHeaders.Add("User-Agent", "C# App");
|
||||
|
||||
string existingVersion = GetLatestVersionFromFile();
|
||||
|
||||
if (string.IsNullOrEmpty(existingVersion)){
|
||||
existingVersion = "v1.0.0";
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(latestVersion)){
|
||||
latestVersion = "v1.0.0";
|
||||
}
|
||||
|
||||
if (existingVersion == latestVersion || Version.Parse(existingVersion.TrimStart('v')) >= Version.Parse(latestVersion.TrimStart('v'))){
|
||||
Console.WriteLine("CHANGELOG.md is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
try{
|
||||
string jsonResponse = await client.GetStringAsync(apiEndpoint); // + "?per_page=100&page=1"
|
||||
|
||||
var releases = Helpers.Deserialize<List<GithubRelease>>(jsonResponse, null);
|
||||
|
||||
// Filter out pre-releases
|
||||
if (releases != null){
|
||||
releases = releases.Where(r => !r.Prerelease).ToList();
|
||||
|
||||
if (releases.Count == 0){
|
||||
Console.WriteLine("No stable releases found.");
|
||||
return;
|
||||
}
|
||||
|
||||
var newReleases = releases.TakeWhile(r => r.TagName != existingVersion).ToList();
|
||||
|
||||
if (newReleases.Count == 0){
|
||||
Console.WriteLine("CHANGELOG.md is already up to date.");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Adding {newReleases.Count} new releases to CHANGELOG.md...");
|
||||
|
||||
AppendNewReleasesToChangelog(newReleases);
|
||||
|
||||
Console.WriteLine("CHANGELOG.md updated successfully.");
|
||||
}
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"Error updating changelog: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
private string GetLatestVersionFromFile(){
|
||||
if (!File.Exists(changelogFilePath))
|
||||
return string.Empty;
|
||||
|
||||
string[] lines = File.ReadAllLines(changelogFilePath);
|
||||
foreach (string line in lines){
|
||||
Match match = Regex.Match(line, @"## \[(v?\d+\.\d+\.\d+)\]");
|
||||
if (match.Success)
|
||||
return match.Groups[1].Value;
|
||||
}
|
||||
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
private void AppendNewReleasesToChangelog(List<GithubRelease> newReleases){
|
||||
string existingContent = "";
|
||||
|
||||
if (File.Exists(changelogFilePath)){
|
||||
existingContent = File.ReadAllText(changelogFilePath);
|
||||
}
|
||||
|
||||
string newEntries = "";
|
||||
|
||||
foreach (var release in newReleases){
|
||||
string version = release.TagName;
|
||||
string date = release.PublishedAt.Split('T')[0];
|
||||
string notes = RemoveUnwantedContent(release.Body);
|
||||
|
||||
newEntries += $"## [{version}] - {date}\n\n{notes}\n\n---\n\n";
|
||||
}
|
||||
|
||||
|
||||
if (string.IsNullOrWhiteSpace(existingContent)){
|
||||
File.WriteAllText(changelogFilePath, "# Changelog\n\n" + newEntries);
|
||||
} else{
|
||||
File.WriteAllText(changelogFilePath, "# Changelog\n\n" + newEntries + existingContent.Substring("# Changelog\n\n".Length));
|
||||
}
|
||||
}
|
||||
|
||||
private static string RemoveUnwantedContent(string notes){
|
||||
return Regex.Split(notes, @"##\r\n\r\n### Linux/MacOS Builds", RegexOptions.IgnoreCase)[0].Trim();
|
||||
}
|
||||
|
||||
public async Task DownloadAndUpdateAsync(){
|
||||
try{
|
||||
|
|
@ -216,4 +323,17 @@ public class Updater : INotifyPropertyChanged{
|
|||
OnPropertyChanged(nameof(failed));
|
||||
}
|
||||
}
|
||||
|
||||
public class GithubRelease{
|
||||
[JsonProperty("tag_name")]
|
||||
public string TagName{ get; set; } = string.Empty;
|
||||
|
||||
public dynamic? Assets{ get; set; }
|
||||
public string Body{ get; set; } = string.Empty;
|
||||
|
||||
[JsonProperty("published_at")]
|
||||
public string PublishedAt{ get; set; } = string.Empty;
|
||||
|
||||
public bool Prerelease{ get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
|
|
@ -10,6 +11,7 @@ using CommunityToolkit.Mvvm.Input;
|
|||
using CRD.Downloader;
|
||||
using CRD.Downloader.Crunchyroll;
|
||||
using CRD.Utils;
|
||||
using CRD.Utils.CustomList;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Structs.Crunchyroll;
|
||||
|
|
@ -24,18 +26,23 @@ public partial class DownloadsPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _removeFinished;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private QueueManager _queueManagerIns;
|
||||
|
||||
public DownloadsPageViewModel(){
|
||||
QueueManager.Instance.UpdateDownloadListItems();
|
||||
Items = QueueManager.Instance.DownloadItemModels;
|
||||
QueueManagerIns = QueueManager.Instance;
|
||||
QueueManagerIns.UpdateDownloadListItems();
|
||||
Items = QueueManagerIns.DownloadItemModels;
|
||||
AutoDownload = CrunchyrollManager.Instance.CrunOptions.AutoDownload;
|
||||
RemoveFinished = CrunchyrollManager.Instance.CrunOptions.RemoveFinishedDownload;
|
||||
}
|
||||
|
||||
|
||||
partial void OnAutoDownloadChanged(bool value){
|
||||
CrunchyrollManager.Instance.CrunOptions.AutoDownload = value;
|
||||
if (value){
|
||||
QueueManager.Instance.UpdateDownloadListItems();
|
||||
QueueManagerIns.UpdateDownloadListItems();
|
||||
}
|
||||
|
||||
CfgManager.WriteCrSettings();
|
||||
|
|
@ -48,8 +55,8 @@ public partial class DownloadsPageViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public void ClearQueue(){
|
||||
var items = QueueManager.Instance.Queue;
|
||||
QueueManager.Instance.Queue.Clear();
|
||||
var items = QueueManagerIns.Queue;
|
||||
QueueManagerIns.Queue.Clear();
|
||||
|
||||
foreach (var crunchyEpMeta in items){
|
||||
if (!crunchyEpMeta.DownloadProgress.Done){
|
||||
|
|
@ -65,6 +72,20 @@ public partial class DownloadsPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void RetryQueue(){
|
||||
var items = QueueManagerIns.Queue;
|
||||
|
||||
foreach (var crunchyEpMeta in items){
|
||||
if (crunchyEpMeta.DownloadProgress.Error){
|
||||
crunchyEpMeta.DownloadProgress = new();
|
||||
}
|
||||
}
|
||||
|
||||
QueueManagerIns.UpdateDownloadListItems();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public partial class DownloadItemModel : INotifyPropertyChanged{
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Avalonia;
|
||||
|
|
@ -339,7 +340,6 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
SelectedSeries = null;
|
||||
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -356,7 +356,7 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public void NavToSeries(){
|
||||
if (ProgramManager.FetchingData){
|
||||
if (ProgramManager.FetchingData && SelectedSeries is{ FetchingData: true }){
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -461,8 +461,9 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenFolderDialogAsyncSeason(HistorySeason? season){
|
||||
public async Task OpenFolderDialogAsync(SeasonDialogArgs? seriesArgs){
|
||||
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.");
|
||||
|
|
@ -475,38 +476,19 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
|
||||
if (result.Count > 0){
|
||||
var selectedFolder = result[0];
|
||||
// Do something with the selected folder path
|
||||
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
|
||||
var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString();
|
||||
Console.WriteLine($"Selected folder: {folderPath}");
|
||||
|
||||
if (season != null){
|
||||
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
|
||||
if (seriesArgs?.Season != null){
|
||||
seriesArgs.Season.SeasonDownloadPath = folderPath;
|
||||
CfgManager.UpdateHistoryFile();
|
||||
} else if (seriesArgs?.Series != null){
|
||||
seriesArgs.Series.SeriesDownloadPath = folderPath;
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenFolderDialogAsyncSeries(HistorySeries? series){
|
||||
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];
|
||||
// Do something with the selected folder path
|
||||
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
|
||||
|
||||
if (series != null){
|
||||
series.SeriesDownloadPath = selectedFolder.Path.LocalPath;
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
seriesArgs?.Series?.UpdateSeriesFolderPath();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -534,6 +516,34 @@ public partial class HistoryPageViewModel : ViewModelBase{
|
|||
|
||||
await Task.WhenAll(downloadTasks);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void ToggleDownloadedMark(SeasonDialogArgs seriesArgs){
|
||||
if (seriesArgs.Season != null){
|
||||
bool allDownloaded = seriesArgs.Season.EpisodesList.All(ep => ep.WasDownloaded);
|
||||
|
||||
foreach (var historyEpisode in seriesArgs.Season.EpisodesList){
|
||||
if (historyEpisode.WasDownloaded == allDownloaded){
|
||||
seriesArgs.Season.UpdateDownloaded(historyEpisode.EpisodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
seriesArgs.Series?.UpdateNewEpisodes();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void OpenFolderPath(HistorySeries? series){
|
||||
try{
|
||||
Process.Start(new ProcessStartInfo{
|
||||
FileName = series?.SeriesFolderPath,
|
||||
UseShellExecute = true,
|
||||
Verb = "open"
|
||||
});
|
||||
} catch (Exception ex){
|
||||
Console.Error.WriteLine($"An error occurred while opening the folder: {ex.Message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class HistoryPageProperties{
|
||||
|
|
|
|||
|
|
@ -43,13 +43,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private string _availableSubs;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _seriesFolderPath;
|
||||
|
||||
[ObservableProperty]
|
||||
public bool _seriesFolderPathExists;
|
||||
|
||||
|
||||
public SeriesPageViewModel(){
|
||||
_storageProvider = ProgramManager.Instance.StorageProvider ?? throw new ArgumentNullException(nameof(ProgramManager.Instance.StorageProvider));
|
||||
|
||||
|
|
@ -79,60 +73,10 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
AvailableDubs = "Available Dubs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableDubLang);
|
||||
AvailableSubs = "Available Subs: " + string.Join(", ", SelectedSeries.HistorySeriesAvailableSoftSubs);
|
||||
|
||||
UpdateSeriesFolderPath();
|
||||
SelectedSeries.UpdateSeriesFolderPath();
|
||||
}
|
||||
|
||||
private void UpdateSeriesFolderPath(){
|
||||
var season = SelectedSeries.Seasons.FirstOrDefault(season => !string.IsNullOrEmpty(season.SeasonDownloadPath));
|
||||
|
||||
if (!string.IsNullOrEmpty(SelectedSeries.SeriesDownloadPath) && Directory.Exists(SelectedSeries.SeriesDownloadPath)){
|
||||
SeriesFolderPath = SelectedSeries.SeriesDownloadPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
|
||||
if (season is{ SeasonDownloadPath: not null }){
|
||||
try{
|
||||
var seasonPath = season.SeasonDownloadPath;
|
||||
var directoryInfo = new DirectoryInfo(seasonPath);
|
||||
|
||||
if (!string.IsNullOrEmpty(directoryInfo.Parent?.FullName)){
|
||||
string parentFolderPath = directoryInfo.Parent?.FullName ?? string.Empty;
|
||||
|
||||
if (Directory.Exists(parentFolderPath)){
|
||||
SeriesFolderPath = parentFolderPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
}
|
||||
} catch (Exception e){
|
||||
Console.Error.WriteLine($"An error occurred while opening the folder: {e.Message}");
|
||||
}
|
||||
} else{
|
||||
string customPath;
|
||||
|
||||
if (string.IsNullOrEmpty(SelectedSeries.SeriesTitle))
|
||||
return;
|
||||
|
||||
var seriesTitle = FileNameManager.CleanupFilename(SelectedSeries.SeriesTitle);
|
||||
|
||||
if (string.IsNullOrEmpty(seriesTitle))
|
||||
return;
|
||||
|
||||
// Check Crunchyroll download directory
|
||||
var downloadDirPath = CrunchyrollManager.Instance.CrunOptions.DownloadDirPath;
|
||||
if (!string.IsNullOrEmpty(downloadDirPath)){
|
||||
customPath = Path.Combine(downloadDirPath, seriesTitle);
|
||||
} else{
|
||||
// Fallback to configured VIDEOS_DIR path
|
||||
customPath = Path.Combine(CfgManager.PathVIDEOS_DIR, seriesTitle);
|
||||
}
|
||||
|
||||
// Check if custom path exists
|
||||
if (Directory.Exists(customPath)){
|
||||
SeriesFolderPath = customPath;
|
||||
SeriesFolderPathExists = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenFolderDialogAsync(HistorySeason? season){
|
||||
|
|
@ -148,19 +92,19 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
|
||||
if (result.Count > 0){
|
||||
var selectedFolder = result[0];
|
||||
// Do something with the selected folder path
|
||||
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
|
||||
var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString();
|
||||
Console.WriteLine($"Selected folder: {folderPath}");
|
||||
|
||||
if (season != null){
|
||||
season.SeasonDownloadPath = selectedFolder.Path.LocalPath;
|
||||
season.SeasonDownloadPath = folderPath;
|
||||
CfgManager.UpdateHistoryFile();
|
||||
} else{
|
||||
SelectedSeries.SeriesDownloadPath = selectedFolder.Path.LocalPath;
|
||||
SelectedSeries.SeriesDownloadPath = folderPath;
|
||||
CfgManager.UpdateHistoryFile();
|
||||
}
|
||||
}
|
||||
|
||||
UpdateSeriesFolderPath();
|
||||
SelectedSeries.UpdateSeriesFolderPath();
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -277,7 +221,7 @@ public partial class SeriesPageViewModel : ViewModelBase{
|
|||
public void OpenFolderPath(){
|
||||
try{
|
||||
Process.Start(new ProcessStartInfo{
|
||||
FileName = SeriesFolderPath,
|
||||
FileName = SelectedSeries.SeriesFolderPath,
|
||||
UseShellExecute = true,
|
||||
Verb = "open"
|
||||
});
|
||||
|
|
|
|||
|
|
@ -197,8 +197,18 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
|
||||
SelectedSeason.Clear();
|
||||
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
foreach (var anilistSeries in list){
|
||||
SelectedSeason.Add(anilistSeries);
|
||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||
var crunchySeries = crunchySimul.Data.FirstOrDefault(ele => ele.Id == anilistSeries.CrunchyrollID);
|
||||
if (crunchySeries != null){
|
||||
anilistSeries.AudioLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.AudioLocales ??[]));
|
||||
anilistSeries.SubtitleLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.SubtitleLocales ??[]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SortItems();
|
||||
|
|
@ -212,8 +222,18 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
|
||||
var list = await GetSeriesForSeason(currentSelection.Season, currentSelection.Year, false);
|
||||
SelectedSeason.Clear();
|
||||
|
||||
var crunchySimul = await CrunchyrollManager.Instance.CrSeries.GetSeasonalSeries(currentSelection.Season, currentSelection.Year + "", "");
|
||||
|
||||
foreach (var anilistSeries in list){
|
||||
SelectedSeason.Add(anilistSeries);
|
||||
if (!string.IsNullOrEmpty(anilistSeries.CrunchyrollID) && crunchySimul?.Data is{ Count: > 0 }){
|
||||
var crunchySeries = crunchySimul.Data.FirstOrDefault(ele => ele.Id == anilistSeries.CrunchyrollID);
|
||||
if (crunchySeries != null){
|
||||
anilistSeries.AudioLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.AudioLocales ??[]));
|
||||
anilistSeries.SubtitleLocales.AddRange(Languages.LocalListToLangList(crunchySeries.SeriesMetadata.SubtitleLocales ??[]));
|
||||
}
|
||||
}
|
||||
}
|
||||
SortItems();
|
||||
}
|
||||
|
|
@ -411,6 +431,7 @@ public partial class UpcomingPageViewModel : ViewModelBase{
|
|||
}
|
||||
|
||||
public void SelectionChangedOfSeries(AnilistSeries? value){
|
||||
if (value != null) value.IsExpanded = !value.IsExpanded;
|
||||
SelectedSeries = null;
|
||||
SelectedIndex = -1;
|
||||
}
|
||||
|
|
|
|||
188
CRD/ViewModels/UpdateViewModel.cs
Normal file
188
CRD/ViewModels/UpdateViewModel.cs
Normal file
|
|
@ -0,0 +1,188 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text.RegularExpressions;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Styling;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CRD.Downloader;
|
||||
using CRD.Utils.Updater;
|
||||
using Markdig;
|
||||
|
||||
namespace CRD.ViewModels;
|
||||
|
||||
public partial class UpdateViewModel : ViewModelBase{
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _updateAvailable;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _updating;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _progress;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _failed;
|
||||
|
||||
private AccountPageViewModel accountPageViewModel;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _changelogText = "<p><strong>No changelog found.</strong></p>";
|
||||
|
||||
[ObservableProperty]
|
||||
private string _currentVersion;
|
||||
|
||||
public UpdateViewModel(){
|
||||
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
|
||||
|
||||
LoadChangelog();
|
||||
|
||||
UpdateAvailable = ProgramManager.Instance.UpdateAvailable;
|
||||
|
||||
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public void StartUpdate(){
|
||||
Updating = true;
|
||||
ProgramManager.Instance.NavigationLock = true;
|
||||
// Title = "Updating";
|
||||
_ = Updater.Instance.DownloadAndUpdateAsync();
|
||||
}
|
||||
|
||||
private void Progress_PropertyChanged(object? sender, PropertyChangedEventArgs e){
|
||||
if (e.PropertyName == nameof(Updater.Instance.progress)){
|
||||
Progress = Updater.Instance.progress;
|
||||
} else if (e.PropertyName == nameof(Updater.Instance.failed)){
|
||||
Failed = Updater.Instance.failed;
|
||||
ProgramManager.Instance.NavigationLock = !Failed;
|
||||
}
|
||||
}
|
||||
|
||||
private void LoadChangelog(){
|
||||
string changelogPath = "CHANGELOG.md";
|
||||
|
||||
if (!File.Exists(changelogPath)){
|
||||
ChangelogText = "<p><strong>No changelog found.</strong></p>";
|
||||
return;
|
||||
}
|
||||
|
||||
string markdownText = File.ReadAllText(changelogPath);
|
||||
|
||||
markdownText = PreprocessMarkdown(markdownText);
|
||||
|
||||
var pipeline = new MarkdownPipelineBuilder()
|
||||
.UseAdvancedExtensions()
|
||||
.UseSoftlineBreakAsHardlineBreak()
|
||||
.Build();
|
||||
|
||||
string htmlContent = Markdown.ToHtml(markdownText, pipeline);
|
||||
|
||||
htmlContent = MakeIssueLinksClickable(htmlContent);
|
||||
htmlContent = ModifyImages(htmlContent);
|
||||
|
||||
Color themeTextColor = Application.Current?.RequestedThemeVariant == ThemeVariant.Dark ? Colors.White : Color.Parse("#E4000000");
|
||||
string cssColor = $"#{themeTextColor.R:X2}{themeTextColor.G:X2}{themeTextColor.B:X2}";
|
||||
|
||||
string styledHtml = $@"
|
||||
<html>
|
||||
<head>
|
||||
<style type=""text/css"">
|
||||
body {{
|
||||
color: {cssColor};
|
||||
background: transparent;
|
||||
font-family: Arial, sans-serif;
|
||||
}}
|
||||
img {{
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 10px auto;
|
||||
max-height: 300px;
|
||||
object-fit: contain;
|
||||
}}
|
||||
li {{
|
||||
margin-bottom: 10px;
|
||||
line-height: 1.6;
|
||||
}}
|
||||
code {{
|
||||
background: #f0f0f0;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
display: inline-block;
|
||||
}}
|
||||
pre code {{
|
||||
background: #f5f5f5;
|
||||
display: block;
|
||||
padding: 10px;
|
||||
border-radius: 5px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: Consolas, Monaco, 'Courier New', monospace;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{htmlContent}
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
ChangelogText = styledHtml;
|
||||
}
|
||||
|
||||
private string MakeIssueLinksClickable(string htmlContent){
|
||||
// Match GitHub issue links
|
||||
string issuePattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/issues\/(\d+))['""][^>]*>[^<]+<\/a>";
|
||||
|
||||
// Match GitHub discussion links
|
||||
string discussionPattern = @"<a href=['""](https:\/\/github\.com\/Crunchy-DL\/Crunchy-Downloader\/discussions\/(\d+))['""][^>]*>[^<]+<\/a>";
|
||||
|
||||
htmlContent = Regex.Replace(htmlContent, issuePattern, match => {
|
||||
string fullUrl = match.Groups[1].Value;
|
||||
string issueNumber = match.Groups[2].Value;
|
||||
return $"<a href='{fullUrl}' target='_blank'>#{issueNumber}</a>";
|
||||
});
|
||||
|
||||
htmlContent = Regex.Replace(htmlContent, discussionPattern, match => {
|
||||
string fullUrl = match.Groups[1].Value;
|
||||
string discussionNumber = match.Groups[2].Value;
|
||||
return $"<a href='{fullUrl}' target='_blank'>#{discussionNumber}</a>";
|
||||
});
|
||||
|
||||
return htmlContent;
|
||||
}
|
||||
|
||||
|
||||
private string ModifyImages(string htmlContent){
|
||||
// Regex to match <img> tags
|
||||
string imgPattern = @"<img\s+src=['""]([^'""]+)['""]( alt=['""]([^'""]+)['""])?\s*\/?>";
|
||||
|
||||
return Regex.Replace(htmlContent, imgPattern, match => {
|
||||
string imgUrl = match.Groups[1].Value;
|
||||
string altText = "View Image"; // match.Groups[3].Success ? match.Groups[3].Value : "View Image";
|
||||
|
||||
return $"<a href='{imgUrl}' target='_blank'>{altText}</a>";
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private string PreprocessMarkdown(string markdownText){
|
||||
// Regex to match <details> blocks containing an image
|
||||
string detailsPattern = @"<details>\s*<summary>.*?<\/summary>\s*<img\s+src=['""]([^'""]+)['""]\s+alt=['""]([^'""]+)['""]\s*\/?>\s*<\/details>";
|
||||
|
||||
return Regex.Replace(markdownText, detailsPattern, match => {
|
||||
string imageUrl = match.Groups[1].Value;
|
||||
string altText = match.Groups[2].Value;
|
||||
|
||||
return $"";
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
using System;
|
||||
using System.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CRD.Utils.Updater;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
|
||||
namespace CRD.ViewModels.Utils;
|
||||
|
||||
public partial class ContentDialogUpdateViewModel : ViewModelBase{
|
||||
private readonly ContentDialog dialog;
|
||||
|
||||
[ObservableProperty]
|
||||
private double _progress;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _failed;
|
||||
|
||||
private AccountPageViewModel accountPageViewModel;
|
||||
|
||||
public ContentDialogUpdateViewModel(ContentDialog dialog){
|
||||
if (dialog is null){
|
||||
throw new ArgumentNullException(nameof(dialog));
|
||||
}
|
||||
|
||||
this.dialog = dialog;
|
||||
dialog.Closed += DialogOnClosed;
|
||||
Updater.Instance.PropertyChanged += Progress_PropertyChanged;
|
||||
}
|
||||
|
||||
private void Progress_PropertyChanged(object? sender, PropertyChangedEventArgs e){
|
||||
if (e.PropertyName == nameof(Updater.Instance.progress)){
|
||||
Progress = Updater.Instance.progress;
|
||||
}else if (e.PropertyName == nameof(Updater.Instance.failed)){
|
||||
Failed = Updater.Instance.failed;
|
||||
dialog.IsPrimaryButtonEnabled = !Failed;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private void DialogOnClosed(ContentDialog sender, ContentDialogClosedEventArgs args){
|
||||
dialog.Closed -= DialogOnClosed;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,10 +38,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
[ObservableProperty]
|
||||
private bool _historyIncludeCrArtists;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _historyAddSpecials;
|
||||
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _historySkipUnmonitored;
|
||||
|
||||
|
|
@ -193,6 +193,12 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
[ObservableProperty]
|
||||
private string _tempDownloadDirPath;
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _downloadFinishedPlaySound;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _downloadFinishedSoundPath;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _currentIp = "";
|
||||
|
||||
|
|
@ -208,7 +214,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
_currentVersion = $"{version?.Major}.{version?.Minor}.{version?.Build}";
|
||||
|
||||
_faTheme = App.Current.Styles[0] as FluentAvaloniaTheme;
|
||||
_faTheme = Application.Current?.Styles[0] as FluentAvaloniaTheme ??[];
|
||||
|
||||
if (CrunchyrollManager.Instance.CrunOptions.AccentColor != null && !string.IsNullOrEmpty(CrunchyrollManager.Instance.CrunOptions.AccentColor)){
|
||||
CustomAccentColor = Color.Parse(CrunchyrollManager.Instance.CrunOptions.AccentColor);
|
||||
|
|
@ -221,6 +227,10 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
BackgroundImageBlurRadius = options.BackgroundImageBlurRadius;
|
||||
BackgroundImageOpacity = options.BackgroundImageOpacity;
|
||||
BackgroundImagePath = options.BackgroundImagePath ?? string.Empty;
|
||||
|
||||
DownloadFinishedSoundPath = options.DownloadFinishedSoundPath ?? string.Empty;
|
||||
DownloadFinishedPlaySound = options.DownloadFinishedPlaySound;
|
||||
|
||||
DownloadDirPath = string.IsNullOrEmpty(options.DownloadDirPath) ? CfgManager.PathVIDEOS_DIR : options.DownloadDirPath;
|
||||
TempDownloadDirPath = string.IsNullOrEmpty(options.DownloadTempDirPath) ? CfgManager.PathTEMP_DIR : options.DownloadTempDirPath;
|
||||
|
||||
|
|
@ -269,6 +279,8 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
return;
|
||||
}
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedPlaySound = DownloadFinishedPlaySound;
|
||||
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageBlurRadius = Math.Clamp((BackgroundImageBlurRadius ?? 0), 0, 40);
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImageOpacity = Math.Clamp((BackgroundImageOpacity ?? 0), 0, 1);
|
||||
|
||||
|
|
@ -371,14 +383,17 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
if (result.Count > 0){
|
||||
var selectedFolder = result[0];
|
||||
Console.WriteLine($"Selected folder: {selectedFolder.Path.LocalPath}");
|
||||
pathSetter(selectedFolder.Path.LocalPath);
|
||||
var folderPath = selectedFolder.Path.IsAbsoluteUri ? selectedFolder.Path.LocalPath : selectedFolder.Path.ToString();
|
||||
Console.WriteLine($"Selected folder: {folderPath}");
|
||||
pathSetter(folderPath);
|
||||
var finalPath = string.IsNullOrEmpty(pathGetter()) ? defaultPath : pathGetter();
|
||||
pathSetter(finalPath);
|
||||
CfgManager.WriteCrSettings();
|
||||
}
|
||||
}
|
||||
|
||||
#region Background Image
|
||||
|
||||
[RelayCommand]
|
||||
public void ClearBackgroundImagePath(){
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = string.Empty;
|
||||
|
|
@ -388,7 +403,13 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
|
||||
[RelayCommand]
|
||||
public async Task OpenImageFileDialogAsyncInternalBackgroundImage(){
|
||||
await OpenImageFileDialogAsyncInternal(
|
||||
await OpenFileDialogAsyncInternal(
|
||||
title: "Select Image File",
|
||||
fileTypes: new List<FilePickerFileType>{
|
||||
new("Image Files"){
|
||||
Patterns = new[]{ "*.png", "*.jpg", "*.jpeg", "*.bmp", "*.gif" }
|
||||
}
|
||||
},
|
||||
pathSetter: (path) => {
|
||||
CrunchyrollManager.Instance.CrunOptions.BackgroundImagePath = path;
|
||||
BackgroundImagePath = path;
|
||||
|
|
@ -399,20 +420,51 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
);
|
||||
}
|
||||
|
||||
private async Task OpenImageFileDialogAsyncInternal(Action<string> pathSetter, Func<string> pathGetter, string defaultPath){
|
||||
#endregion
|
||||
|
||||
|
||||
#region Download Finished Sound
|
||||
|
||||
[RelayCommand]
|
||||
public void ClearFinishedSoundPath(){
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = string.Empty;
|
||||
DownloadFinishedSoundPath = string.Empty;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
public async Task OpenImageFileDialogAsyncInternalFinishedSound(){
|
||||
await OpenFileDialogAsyncInternal(
|
||||
title: "Select Audio File",
|
||||
fileTypes: new List<FilePickerFileType>{
|
||||
new("Audio Files"){
|
||||
Patterns = new[]{ "*.mp3", "*.wav", "*.ogg", "*.flac", "*.aac" }
|
||||
}
|
||||
},
|
||||
pathSetter: (path) => {
|
||||
CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath = path;
|
||||
DownloadFinishedSoundPath = path;
|
||||
},
|
||||
pathGetter: () => CrunchyrollManager.Instance.CrunOptions.DownloadFinishedSoundPath,
|
||||
defaultPath: string.Empty
|
||||
);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private async Task OpenFileDialogAsyncInternal(
|
||||
string title,
|
||||
List<FilePickerFileType> fileTypes,
|
||||
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" }
|
||||
}
|
||||
},
|
||||
Title = title,
|
||||
FileTypeFilter = fileTypes,
|
||||
AllowMultiple = false
|
||||
});
|
||||
|
||||
|
|
@ -426,6 +478,7 @@ public partial class GeneralSettingsViewModel : ViewModelBase{
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
partial void OnCurrentAppThemeChanged(ComboBoxItem? value){
|
||||
if (value?.Content?.ToString() == "System"){
|
||||
_faTheme.PreferSystemTheme = true;
|
||||
|
|
|
|||
|
|
@ -23,15 +23,32 @@
|
|||
<StackPanel Grid.Row="0" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<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>
|
||||
<Button BorderThickness="0"
|
||||
HorizontalAlignment="Right"
|
||||
|
||||
<Button BorderThickness="0"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 0 10 0 "
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ClearQueue}">
|
||||
IsEnabled="{Binding QueueManagerIns.HasFailedItem}"
|
||||
Command="{Binding RetryQueue}">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<controls:SymbolIcon Symbol="Refresh" FontSize="22" />
|
||||
</StackPanel>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Retry failed" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap"></TextBlock>
|
||||
</ToolTip.Tip>
|
||||
</Button>
|
||||
|
||||
<Button BorderThickness="0"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="0 0 10 0 "
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding ClearQueue}">
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<controls:SymbolIcon Symbol="Delete" FontSize="22" />
|
||||
<TextBlock Text="Clear Queue" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" ></TextBlock>
|
||||
</StackPanel>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Clear Queue" HorizontalAlignment="Center" VerticalAlignment="Center" TextWrapping="Wrap" ></TextBlock>
|
||||
</ToolTip.Tip>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
using CRD.ViewModels;
|
||||
|
||||
namespace CRD.Views;
|
||||
|
||||
|
|
@ -6,4 +8,5 @@ public partial class DownloadsPageView : UserControl{
|
|||
public DownloadsPageView(){
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -7,7 +7,6 @@
|
|||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:history="clr-namespace:CRD.Utils.Structs.History"
|
||||
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
||||
xmlns:local="clr-namespace:CRD.Utils"
|
||||
x:DataType="vm:HistoryPageViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.HistoryPageView">
|
||||
|
|
@ -15,7 +14,11 @@
|
|||
<UserControl.Resources>
|
||||
<ui:UiIntToVisibilityConverter x:Key="UiIntToVisibilityConverter" />
|
||||
<ui:UiSonarrIdToVisibilityConverter x:Key="UiSonarrIdToVisibilityConverter" />
|
||||
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
|
||||
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
|
||||
<ui:UiSeriesSeasonConverter x:Key="UiSeriesSeasonConverter"/>
|
||||
</UserControl.Resources>
|
||||
|
||||
|
||||
<Grid>
|
||||
|
||||
|
|
@ -25,7 +28,7 @@
|
|||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" /> <!-- Takes up most space for the title -->
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
|
@ -393,13 +396,26 @@
|
|||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="15" TextWrapping="Wrap"
|
||||
<TextBlock Grid.Row="0" FontSize="25" Text="{Binding SeriesTitle}" TextTrimming="CharacterEllipsis"></TextBlock>
|
||||
<TextBlock Grid.Row="1" FontSize="15" Margin="0 0 0 5" TextWrapping="Wrap"
|
||||
Text="{Binding SeriesDescription}">
|
||||
</TextBlock>
|
||||
<StackPanel Grid.Row="3" Orientation="Vertical">
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" Text="Available Dubs: " />
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableDubLang, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" IsVisible="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<TextBlock FontSize="15" Opacity="0.8" Text="Available Subs: " />
|
||||
<TextBlock FontSize="15" Opacity="0.8" TextWrapping="Wrap" Text="{Binding HistorySeriesAvailableSoftSubs, Converter={StaticResource UiListToStringConverter}}"></TextBlock>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<StackPanel Grid.Row="5" Orientation="Vertical">
|
||||
|
||||
<StackPanel Orientation="Horizontal" Margin="0 10 10 10">
|
||||
|
||||
|
|
@ -427,6 +443,20 @@
|
|||
Height="30" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
|
||||
BorderThickness="0" CornerRadius="50"
|
||||
IsVisible="{Binding SeriesFolderPathExists}"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderPath}"
|
||||
CommandParameter="{Binding .}">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Open Series Folder" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
<Grid>
|
||||
<!-- <controls:ImageIcon Source="../Assets/sonarr.png" Width="30" Height="30" /> -->
|
||||
<controls:SymbolIcon Symbol="Folder" FontSize="20" Width="30" Height="30" />
|
||||
</Grid>
|
||||
</Button>
|
||||
|
||||
|
||||
</StackPanel>
|
||||
|
|
@ -437,8 +467,13 @@
|
|||
|
||||
<Button Margin="0 0 5 10" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsyncSeries}"
|
||||
CommandParameter="{Binding .}">
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
|
||||
>
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding SeriesDownloadPath}"
|
||||
FontSize="15" />
|
||||
|
|
@ -613,7 +648,7 @@
|
|||
|
||||
|
||||
<controls:SettingsExpanderItem>
|
||||
<ScrollViewer>
|
||||
<ScrollViewer x:Name="TableViewScrollViewer">
|
||||
<ItemsControl ItemsSource="{Binding Seasons}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate DataType="{x:Type history:HistorySeason}">
|
||||
|
|
@ -705,6 +740,19 @@
|
|||
FontSize="18" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button Margin="0 0 5 0" FontStyle="Italic" HorizontalAlignment="Right"
|
||||
VerticalAlignment="Center"
|
||||
IsEnabled="{Binding HistoryEpisodeAvailableSoftSubs, Converter={StaticResource UiListHasElementsConverter}}"
|
||||
Command="{Binding DownloadEpisode}"
|
||||
CommandParameter="true">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<controls:SymbolIcon Symbol="ClosedCaption" FontSize="18" />
|
||||
</StackPanel>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Download Subs" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
|
@ -770,16 +818,34 @@
|
|||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).DownloadSeasonMissingSonarr}"
|
||||
CommandParameter="{Binding }">
|
||||
</Button>
|
||||
<Button Margin="10 5"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Stretch"
|
||||
Content="Toggle Downloaded"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).ToggleDownloadedMark}">
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding Path="DataContext" ElementName="TableViewScrollViewer" />
|
||||
<Binding />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
</Button>
|
||||
</StackPanel>
|
||||
|
||||
</Border>
|
||||
</Popup>
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<Button Margin="10 0 0 0" FontStyle="Italic"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsyncSeason}"
|
||||
CommandParameter="{Binding .}">
|
||||
Command="{Binding $parent[UserControl].((vm:HistoryPageViewModel)DataContext).OpenFolderDialogAsync}"
|
||||
>
|
||||
<Button.CommandParameter>
|
||||
<MultiBinding Converter="{StaticResource UiSeriesSeasonConverter}">
|
||||
<Binding Path="DataContext" RelativeSource="{RelativeSource AncestorType=ScrollViewer}" />
|
||||
<Binding />
|
||||
</MultiBinding>
|
||||
</Button.CommandParameter>
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="{Binding SeasonDownloadPath}"
|
||||
FontSize="15" />
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@
|
|||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
||||
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||
xmlns:views="clr-namespace:CRD.Views"
|
||||
xmlns:ui1="clr-namespace:CRD.Utils.UI"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
|
|
@ -68,8 +69,8 @@
|
|||
IconSource="Library" />
|
||||
</ui:NavigationView.MenuItems>
|
||||
<ui:NavigationView.FooterMenuItems>
|
||||
<ui:NavigationViewItem Classes="SampleAppNav" Content="Update Available" Tag="UpdateAvailable"
|
||||
IconSource="CloudDownload" Focusable="False" IsEnabled="{Binding ProgramManager.UpdateAvailable}" />
|
||||
<ui:NavigationViewItem Classes="SampleAppNav" Content="Update" Tag="Update" Opacity="{Binding ProgramManager.OpacityButton}"
|
||||
IconSource="CloudDownload" Focusable="False" />
|
||||
<ui:NavigationViewItem Classes="SampleAppNav" Content="Account" Tag="Account"
|
||||
IconSource="Contact" />
|
||||
<ui:NavigationViewItem Classes="SampleAppNav" Content="Settings" Tag="Settings"
|
||||
|
|
|
|||
|
|
@ -10,14 +10,12 @@ using CRD.Downloader.Crunchyroll;
|
|||
using CRD.Utils;
|
||||
using CRD.Utils.Files;
|
||||
using CRD.Utils.Structs;
|
||||
using CRD.Utils.Updater;
|
||||
using CRD.ViewModels;
|
||||
using CRD.Views.Utils;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using FluentAvalonia.UI.Windowing;
|
||||
using Newtonsoft.Json;
|
||||
using ReactiveUI;
|
||||
using ContentDialogUpdateViewModel = CRD.ViewModels.Utils.ContentDialogUpdateViewModel;
|
||||
using UpdateViewModel = CRD.ViewModels.UpdateViewModel;
|
||||
|
||||
namespace CRD.Views;
|
||||
|
||||
|
|
@ -163,9 +161,9 @@ public partial class MainWindow : AppWindow{
|
|||
navView.Content = viewModel;
|
||||
selectedNavVieItem = selectedItem;
|
||||
break;
|
||||
case "UpdateAvailable":
|
||||
Updater.Instance.DownloadAndUpdateAsync();
|
||||
ShowUpdateDialog();
|
||||
case "Update":
|
||||
navView.Content = Activator.CreateInstance(typeof(UpdateViewModel));
|
||||
selectedNavVieItem = selectedItem;
|
||||
break;
|
||||
default:
|
||||
// (sender as NavigationView).Content = Activator.CreateInstance(typeof(DownloadsPageViewModel));
|
||||
|
|
@ -174,23 +172,7 @@ public partial class MainWindow : AppWindow{
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async void ShowUpdateDialog(){
|
||||
var dialog = new ContentDialog(){
|
||||
Title = "Updating",
|
||||
PrimaryButtonText = "Close"
|
||||
};
|
||||
|
||||
dialog.IsPrimaryButtonEnabled = false;
|
||||
|
||||
var viewModel = new ContentDialogUpdateViewModel(dialog);
|
||||
dialog.Content = new ContentDialogUpdateView(){
|
||||
DataContext = viewModel
|
||||
};
|
||||
|
||||
_ = await dialog.ShowAsync();
|
||||
}
|
||||
|
||||
|
||||
private void OnOpened(object sender, EventArgs e){
|
||||
if (File.Exists(CfgManager.PathWindowSettings)){
|
||||
var settings = JsonConvert.DeserializeObject<WindowSettings>(File.ReadAllText(CfgManager.PathWindowSettings));
|
||||
|
|
|
|||
|
|
@ -89,7 +89,7 @@
|
|||
|
||||
<Button Width="34" Height="34" Margin="0 0 10 0" Background="Transparent"
|
||||
BorderThickness="0" CornerRadius="50"
|
||||
IsVisible="{Binding SeriesFolderPathExists}"
|
||||
IsVisible="{Binding SelectedSeries.SeriesFolderPathExists}"
|
||||
Command="{Binding OpenFolderPath}">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Open Series Folder" FontSize="15" />
|
||||
|
|
|
|||
|
|
@ -5,10 +5,16 @@
|
|||
xmlns:vm="clr-namespace:CRD.ViewModels"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:structs="clr-namespace:CRD.Utils.Structs"
|
||||
xmlns:ui="clr-namespace:CRD.Utils.UI"
|
||||
x:DataType="vm:UpcomingPageViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.UpcomingPageView">
|
||||
|
||||
<UserControl.Resources>
|
||||
<ui:UiListToStringConverter x:Key="UiListToStringConverter" />
|
||||
<ui:UiListHasElementsConverter x:Key="UiListHasElementsConverter" />
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
|
|
@ -140,6 +146,141 @@
|
|||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
|
||||
<Expander Grid.Column="0" Grid.ColumnSpan="2" ExpandDirection="Right" IsExpanded="{Binding IsExpanded}">
|
||||
<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>
|
||||
|
||||
|
||||
<Grid MaxWidth="185" IsVisible="{Binding AudioLocales, Converter={StaticResource UiListHasElementsConverter}}">
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Dubs -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
Text="Dubs: " />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1"
|
||||
FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
Text="{Binding AudioLocales, Converter={StaticResource UiListToStringConverter}}"
|
||||
TextWrapping="NoWrap" />
|
||||
|
||||
<!-- Subs -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
Text="Subs: " />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
FontStyle="Italic"
|
||||
FontSize="12"
|
||||
Opacity="0.8"
|
||||
Text="{Binding SubtitleLocales, Converter={StaticResource UiListToStringConverter}}"
|
||||
TextWrapping="NoWrap" />
|
||||
|
||||
<ToolTip.Tip>
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="Auto" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Dubs -->
|
||||
<TextBlock Grid.Row="0" Grid.Column="0"
|
||||
FontSize="17"
|
||||
Text="Dubs: " />
|
||||
<TextBlock Grid.Row="0" Grid.Column="1"
|
||||
FontSize="17"
|
||||
Text="{Binding AudioLocales, Converter={StaticResource UiListToStringConverter}}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
<!-- Subs -->
|
||||
<TextBlock Grid.Row="1" Grid.Column="0"
|
||||
FontSize="17"
|
||||
Text="Subs: " />
|
||||
<TextBlock Grid.Row="1" Grid.Column="1"
|
||||
FontSize="17"
|
||||
Text="{Binding SubtitleLocales, Converter={StaticResource UiListToStringConverter}}"
|
||||
TextWrapping="Wrap" />
|
||||
|
||||
|
||||
</Grid>
|
||||
</ToolTip.Tip>
|
||||
</Grid>
|
||||
|
||||
<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>
|
||||
|
||||
|
||||
<StackPanel Grid.Column="0" Orientation="Vertical" HorizontalAlignment="Center"
|
||||
Width="185"
|
||||
Height="315"
|
||||
|
|
@ -188,65 +329,6 @@
|
|||
|
||||
</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>
|
||||
|
|
|
|||
83
CRD/Views/UpdateView.axaml
Normal file
83
CRD/Views/UpdateView.axaml
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
<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:avalonia="clr-namespace:TheArtOfDev.HtmlRenderer.Avalonia;assembly=Avalonia.HtmlRenderer"
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
x:DataType="vm:UpdateViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.UpdateView">
|
||||
|
||||
<Grid>
|
||||
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" />
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="3" Background="{DynamicResource ControlAltFillColorQuarternary}"></StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="1">
|
||||
|
||||
<DockPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image Source="/Assets/app_icon.ico"
|
||||
Margin="2.5"
|
||||
DockPanel.Dock="Left"
|
||||
Height="65"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
|
||||
<StackPanel Spacing="0" Margin="12 0" VerticalAlignment="Center">
|
||||
<TextBlock Text="Crunchy-Downloader" />
|
||||
|
||||
<TextBlock Text="{Binding CurrentVersion}" />
|
||||
|
||||
<TextBlock Text="https://github.com/Crunchy-DL/Crunchy-Downloader"
|
||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}" />
|
||||
</StackPanel>
|
||||
</DockPanel>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="0" Grid.Column="2" Orientation="Horizontal">
|
||||
|
||||
<Button Width="70" Height="70" Background="Transparent" BorderThickness="0" Margin="5 0"
|
||||
VerticalAlignment="Center"
|
||||
Command="{Binding StartUpdate}"
|
||||
IsEnabled="{Binding UpdateAvailable}">
|
||||
<StackPanel Orientation="Vertical" HorizontalAlignment="Center">
|
||||
<controls:SymbolIcon Symbol="Download" FontSize="32" />
|
||||
<TextBlock Text="Update" HorizontalAlignment="Center" TextWrapping="Wrap" FontSize="12"></TextBlock>
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
</StackPanel>
|
||||
|
||||
|
||||
<ScrollViewer Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" HorizontalAlignment="Center" IsVisible="{Binding !Updating}" MaxWidth="700"
|
||||
Margin="10"
|
||||
VerticalScrollBarVisibility="Auto"
|
||||
HorizontalScrollBarVisibility="Disabled">
|
||||
<avalonia:HtmlLabel Name="HtmlContent"
|
||||
Text="{Binding ChangelogText}"
|
||||
Margin="10"
|
||||
VerticalAlignment="Stretch" />
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
<!-- Update Progress Section -->
|
||||
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Spacing="10" IsVisible="{Binding Updating}">
|
||||
<TextBlock IsVisible="{Binding !Failed}" Text="Please wait while the update is being downloaded..." HorizontalAlignment="Center" Margin="0,10,0,20" />
|
||||
<TextBlock IsVisible="{Binding Failed}" Foreground="IndianRed" Text="Update failed check the log for more information" HorizontalAlignment="Center" Margin="0,10,0,20" />
|
||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350" />
|
||||
</StackPanel>
|
||||
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
9
CRD/Views/UpdateView.axaml.cs
Normal file
9
CRD/Views/UpdateView.axaml.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace CRD.Views;
|
||||
|
||||
public partial class UpdateView : UserControl{
|
||||
public UpdateView(){
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
<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:utils="clr-namespace:CRD.ViewModels.Utils"
|
||||
x:DataType="utils:ContentDialogUpdateViewModel"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="CRD.Views.Utils.ContentDialogUpdateView">
|
||||
|
||||
<StackPanel Spacing="10" MinWidth="400">
|
||||
<TextBlock IsVisible="{Binding !Failed}" Text="Please wait while the update is being downloaded..." HorizontalAlignment="Center" Margin="0,10,0,20"/>
|
||||
<TextBlock IsVisible="{Binding Failed}" Foreground="IndianRed" Text="Update failed check the log for more information" HorizontalAlignment="Center" Margin="0,10,0,20"/>
|
||||
<ProgressBar Minimum="0" Maximum="100" Value="{Binding Progress}" HorizontalAlignment="Center" VerticalAlignment="Center" Width="350"/>
|
||||
</StackPanel>
|
||||
</UserControl>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
using Avalonia.Controls;
|
||||
|
||||
namespace CRD.Views.Utils;
|
||||
|
||||
public partial class ContentDialogUpdateView : UserControl{
|
||||
public ContentDialogUpdateView(){
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
|
|
@ -14,7 +14,7 @@
|
|||
</Design.DataContext>
|
||||
|
||||
|
||||
<ScrollViewer Padding="20 20 20 0" >
|
||||
<ScrollViewer Padding="20 20 20 0">
|
||||
<StackPanel Spacing="8">
|
||||
|
||||
<controls:SettingsExpander Header="History"
|
||||
|
|
@ -38,19 +38,19 @@
|
|||
<CheckBox IsChecked="{Binding HistoryIncludeCrArtists}"> </CheckBox>
|
||||
</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:SettingsExpanderItem Content="History Skip Sonarr Unmonitored" Description="Skips unmonitored sonarr episodes when counting Missing/New">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding HistorySkipUnmonitored}"> </CheckBox>
|
||||
|
|
@ -145,6 +145,96 @@
|
|||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Play completion sound" Description="Enables a notification sound to be played when all downloads have finished">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<StackPanel Spacing="10">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
|
||||
<TextBlock IsVisible="{Binding DownloadFinishedPlaySound}"
|
||||
Text="{Binding DownloadFinishedSoundPath, Mode=OneWay}"
|
||||
FontSize="15"
|
||||
Opacity="0.8"
|
||||
TextWrapping="NoWrap"
|
||||
TextAlignment="Center"
|
||||
VerticalAlignment="Center" />
|
||||
|
||||
<Button IsVisible="{Binding DownloadFinishedPlaySound}"
|
||||
Command="{Binding OpenImageFileDialogAsyncInternalFinishedSound}"
|
||||
VerticalAlignment="Center"
|
||||
FontStyle="Italic">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Select Finished Sound" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<controls:SymbolIcon Symbol="Folder" FontSize="18" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<Button IsVisible="{Binding DownloadFinishedPlaySound}"
|
||||
Command="{Binding ClearFinishedSoundPath}"
|
||||
VerticalAlignment="Center"
|
||||
FontStyle="Italic">
|
||||
<ToolTip.Tip>
|
||||
<TextBlock Text="Remove Finished Sound Path" FontSize="15" />
|
||||
</ToolTip.Tip>
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<controls:SymbolIcon Symbol="Clear" FontSize="18" />
|
||||
</StackPanel>
|
||||
</Button>
|
||||
|
||||
<CheckBox IsChecked="{Binding DownloadFinishedPlaySound}"> </CheckBox>
|
||||
</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:SettingsExpander.Footer>
|
||||
</controls:SettingsExpander.Footer>
|
||||
</controls:SettingsExpander>
|
||||
|
|
@ -201,13 +291,13 @@
|
|||
<CheckBox IsChecked="{Binding ProxyEnabled}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Use Socks5 Proxy">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<CheckBox IsChecked="{Binding ProxySocks}"> </CheckBox>
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Host">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
|
|
@ -225,7 +315,7 @@
|
|||
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Username">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
|
|
@ -233,7 +323,7 @@
|
|||
|
||||
</controls:SettingsExpanderItem.Footer>
|
||||
</controls:SettingsExpanderItem>
|
||||
|
||||
|
||||
<controls:SettingsExpanderItem Content="Password">
|
||||
<controls:SettingsExpanderItem.Footer>
|
||||
<TextBox HorizontalAlignment="Left" MinWidth="250"
|
||||
|
|
|
|||
Loading…
Reference in a new issue