From 09ac7befe98a1ca614f076c26a3c7b946ad10fff Mon Sep 17 00:00:00 2001 From: ahjephson Date: Wed, 12 Jun 2024 13:04:23 +0100 Subject: [PATCH] Rework actions to mirror original implmentation and add missing torrent actions --- .../Components/TorrentActions.razor | 57 ++- .../Components/TorrentActions.razor.cs | 351 ++++++++++++++---- Lantean.QBTMudBlade/DialogHelper.cs | 21 +- Lantean.QBTMudBlade/Extensions.cs | 10 + Lantean.QBTMudBlade/Models/ContentItem.cs | 2 +- Lantean.QBTMudBlade/Pages/Login.razor.cs | 4 +- Lantean.QBTMudBlade/readme.md | 9 + 7 files changed, 354 insertions(+), 100 deletions(-) create mode 100644 Lantean.QBTMudBlade/readme.md diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor b/Lantean.QBTMudBlade/Components/TorrentActions.razor index ae47297..2ada44e 100644 --- a/Lantean.QBTMudBlade/Components/TorrentActions.razor +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor @@ -22,34 +22,30 @@ else if (RenderType == RenderType.InitialIconsOnly) { @foreach (var action in Actions.Take(5)) { - @if (action is Divider) + @if (action.SeparatorBefore) { } - else - { - - } + + } @Menu(Actions.Skip(5)) } else if (RenderType == RenderType.Children) { - var parent = Actions.FirstOrDefault(a => a.Name == ParentAction?.Name); + var parent = Actions.FirstOrDefault(a => a.Text == ParentAction?.Text); if (parent is not null) { @foreach (var action in parent.Children) { - @if (action is Divider) + @if (action.SeparatorBefore) { } - else - { - @action.Name - } + + @action.Text } } @@ -68,24 +64,25 @@ else { foreach (var action in Actions) { - if (action is Divider) + if (action.SeparatorBefore) { } - else if (!action.Children.Any()) + + if (!action.Children.Any()) { if (action.Icon is null) { - @action.Name + @action.Text } else { - + } } else { - + @foreach (var childItem in action.Children) { @ChildItem(childItem) @@ -105,24 +102,25 @@ else { foreach (var action in Actions) { - if (action is Divider) + if (action.SeparatorBefore) { } - else if (!action.Children.Any()) + + if (!action.Children.Any()) { if (action.Icon is null) { - @action.Name + @action.Text } else { - + } } else { - + @foreach (var childItem in action.Children) { @ChildItem(childItem) @@ -138,14 +136,12 @@ else { return __builder => { - if (action is Divider) + if (action.SeparatorBefore) { } - else - { - @action.Name - } + + @action.Text }; } @@ -156,14 +152,15 @@ else @foreach (var action in actions) { - @if (action is Divider) + @if (action.SeparatorBefore) { } - else if (!action.Children.Any()) + + if (!action.Children.Any()) { - @action.Name + @action.Text } else @@ -171,7 +168,7 @@ else - @action.Name + @action.Text diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs index 3a7403c..d0e25b2 100644 --- a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs @@ -11,6 +11,8 @@ namespace Lantean.QBTMudBlade.Components { public partial class TorrentActions { + private readonly List _actions; + [Inject] public IApiClient ApiClient { get; set; } = default!; @@ -58,6 +60,47 @@ namespace Lantean.QBTMudBlade.Components protected bool Disabled => !Hashes.Any(); + public TorrentActions() + { + _actions = new List + { + new TorrentAction("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)), + new TorrentAction("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)), + new TorrentAction("forceStart", "Force start", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(ForceStart)), + new TorrentAction("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true), + new TorrentAction("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true), + new TorrentAction("rename", "Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)), + new TorrentAction("renameFiles", "Rename files", Icons.Material.Filled.DriveFileRenameOutline, Color.Warning, CreateCallback(Rename)), + new TorrentAction("category", "Category", Icons.Material.Filled.List, Color.Info, CreateCallback(ShowCategories)), + new TorrentAction("tags", "Tags", Icons.Material.Filled.Label, Color.Info, CreateCallback(ShowTags)), + new TorrentAction("autoTorrentManagement", "Automatic Torrent Management", Icons.Material.Filled.Check, Color.Info, CreateCallback(ToggleAutoTMM)), + new TorrentAction("downloadLimit", "Limit download rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, CreateCallback(LimitDownloadRate), separatorBefore: true), + new TorrentAction("uploadLimit", "Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, CreateCallback(LimitUploadRate)), + new TorrentAction("shareRatio", "Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, CreateCallback(LimitShareRatio)), + new TorrentAction("superSeeding", "Super seeding mode", Icons.Material.Filled.Check, Color.Info, CreateCallback(ToggleSuperSeeding)), + new TorrentAction("sequentialDownload", "Download in sequential order", Icons.Material.Filled.Reorder, Color.Info, CreateCallback(DownloadSequential), separatorBefore: true), + new TorrentAction("firstLastPiecePrio", "Download first and last pieces first", Icons.Material.Filled.Navigation, Color.Info, CreateCallback(DownloadFirstLast)), + new TorrentAction("forceRecheck", "Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck), separatorBefore: true), + new TorrentAction("forceReannounce", "Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)), + new TorrentAction("queue", "Queue", Icons.Material.Filled.Queue, Color.Transparent, new List + { + new TorrentAction("queueTop", "Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)), + new TorrentAction("queueUp", "Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)), + new TorrentAction("queueDown", "Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)), + new TorrentAction("queueBottom", "Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)), + }, separatorBefore: true), + new TorrentAction("copy", "Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List + { + new TorrentAction("copyName", "Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))), + new TorrentAction("copyHashv1", "Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))), + new TorrentAction("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))), + new TorrentAction("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))), + new TorrentAction("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))), + }), + new TorrentAction("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)), + }; + } + protected async Task Pause() { await ApiClient.PauseTorrents(Hashes); @@ -70,6 +113,12 @@ namespace Lantean.QBTMudBlade.Components Snackbar.Add("Torrent resumed."); } + protected async Task ForceStart() + { + await ApiClient.SetForceStart(true, null, Hashes.ToArray()); + Snackbar.Add("Torrent force started."); + } + protected async Task Remove() { await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray()); @@ -117,6 +166,18 @@ namespace Lantean.QBTMudBlade.Components await ApiClient.SetAutomaticTorrentManagement(true, null, torrents.Where(t => !t.AutomaticTorrentManagement).Select(t => t.Hash).ToArray()); } + protected async Task LimitDownloadRate() + { + long downloadLimit = -1; + string hash = Hashes.First(); + if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent)) + { + downloadLimit = torrent.UploadLimit; + } + + await DialogService.InvokeDownloadRateDialog(ApiClient, downloadLimit, Hashes); + } + protected async Task LimitUploadRate() { long uploadLimit = -1; @@ -220,6 +281,16 @@ namespace Lantean.QBTMudBlade.Components await DialogService.ShowAsync("Manage Torrent Categories", parameters, DialogHelper.FormDialogOptions); } + protected async Task DownloadSequential() + { + await ApiClient.ToggleSequentialDownload(null, Hashes.ToArray()); + } + + protected async Task DownloadFirstLast() + { + await ApiClient.SetFirstLastPiecePriority(null, Hashes.ToArray()); + } + protected async Task SubMenuTouch(TorrentAction action) { await DialogService.ShowSubMenu(Hashes, action, MainData, Preferences); @@ -236,71 +307,214 @@ namespace Lantean.QBTMudBlade.Components } } - private List? _actions; + private IEnumerable Actions => GetActions(); - private IEnumerable Actions + private IEnumerable GetActions() { - get + var allAreSequentialDownload = true; + var thereAreSequentialDownload = false; + var allAreFirstLastPiecePrio = true; + var thereAreFirstLastPiecePrio = false; + var allAreDownloaded = true; + var allArePaused = true; + var thereArePaused = false; + var allAreForceStart = true; + var thereAreForceStart = false; + var allAreSuperSeeding = true; + var allAreAutoTmm = true; + var thereAreAutoTmm = false; + + Torrent? firstTorrent = null; + foreach (var torrent in GetTorrents()) { - if (_actions is not null) + if (firstTorrent is null) { - if (Preferences?.QueueingEnabled == false) - { - return _actions.Where(a => a.Name != "Queue"); - } - return _actions; + firstTorrent = torrent; + } + if (!torrent.SequentialDownload) + { + allAreSequentialDownload = false; + } + else + { + thereAreSequentialDownload = true; } - Torrent? torrent = null; - if (Hashes.Any()) + if (!torrent.FirstLastPiecePriority) { - string key = Hashes.First(); - if (!MainData.Torrents.TryGetValue(key, out torrent)) - { - Hashes = Hashes.Except([key]); - } + allAreFirstLastPiecePrio = false; + } + else + { + thereAreFirstLastPiecePrio = true; } - _actions = new List + if (torrent.Progress != 1.0) // not downloaded { - new TorrentAction("Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)), - new TorrentAction("Resume", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)), - new Divider(), - new TorrentAction("Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove)), - new Divider(), - new TorrentAction("Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation)), - new TorrentAction("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)), - new TorrentAction("Category", Icons.Material.Filled.List, Color.Info, CreateCallback(ShowCategories)), - new TorrentAction("Tags", Icons.Material.Filled.Label, Color.Info, CreateCallback(ShowTags)), - new TorrentAction("Automatic Torrent Management", Icons.Material.Filled.Check, (torrent?.AutomaticTorrentManagement == true) ? Color.Info : Color.Transparent, CreateCallback(ToggleAutoTMM)), - new Divider(), - new TorrentAction("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, CreateCallback(LimitUploadRate)), - new TorrentAction("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, CreateCallback(LimitShareRatio)), - new TorrentAction("Super seeding mode", Icons.Material.Filled.Check, (torrent?.SuperSeeding == true) ? Color.Info : Color.Transparent, CreateCallback(ToggleSuperSeeding)), - new Divider(), - new TorrentAction("Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck)), - new TorrentAction("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)), - new Divider(), - new TorrentAction("Queue", Icons.Material.Filled.Queue, Color.Transparent, new List - { - new TorrentAction("Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)), - new TorrentAction("Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)), - new TorrentAction("Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)), - new TorrentAction("Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)), - }), - new TorrentAction("Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List - { - new TorrentAction("Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))), - new TorrentAction("Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))), - new TorrentAction("Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))), - new TorrentAction("Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))), - new TorrentAction("Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))), - }), - new TorrentAction("Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)), - }; + allAreDownloaded = false; + } + else if (!torrent.SuperSeeding) + { + allAreSuperSeeding = false; + } - return _actions; + if (torrent.State != "pausedUP" && torrent.State != "pausedDL") + { + allArePaused = false; + } + else + { + thereArePaused = true; + } + + if (!torrent.ForceStart) + { + allAreForceStart = false; + } + else + { + thereAreForceStart = true; + } + + if (torrent.AutomaticTorrentManagement) + { + thereAreAutoTmm = true; + } + else + { + allAreAutoTmm = false; + } } + + bool showSequentialDownload = true; + if (!allAreSequentialDownload && thereAreSequentialDownload) + { + showSequentialDownload = false; + } + + bool showAreFirstLastPiecePrio = true; + if (!allAreFirstLastPiecePrio && thereAreFirstLastPiecePrio) + { + showAreFirstLastPiecePrio = false; + } + + var actionStates = new Dictionary(); + + var showRenameFiles = Hashes.Count() == 1 && firstTorrent!.MetaDownloaded(); + if (!showRenameFiles) + { + actionStates["renameFiles"] = ActionState.Hidden; + } + + if (allAreDownloaded) + { + actionStates["downloadLimit"] = ActionState.Hidden; + actionStates["uploadLimit"] = ActionState.HasSeperator; + actionStates["sequentialDownload"] = ActionState.Hidden; + actionStates["firstLastPiecePrio"] = ActionState.Hidden; + actionStates["superSeeding"] = new ActionState { IsChecked = allAreSuperSeeding }; + } + else + { + if (!showSequentialDownload && showAreFirstLastPiecePrio) + { + actionStates["firstLastPiecePrio"] = ActionState.HasSeperator; + } + + if (!showSequentialDownload) + { + actionStates["sequentialDownload"] = ActionState.Hidden; + } + + if (!showAreFirstLastPiecePrio) + { + actionStates["firstLastPiecePrio"] = ActionState.Hidden; + } + + if (!actionStates.TryGetValue("sequentialDownload", out var sequentialDownload)) + { + actionStates["sequentialDownload"] = new ActionState { IsChecked = allAreSequentialDownload }; + } + else + { + sequentialDownload.IsChecked = allAreSequentialDownload; + } + + if (!actionStates.TryGetValue("firstLastPiecePrio", out var firstLastPiecePrio)) + { + actionStates["firstLastPiecePrio"] = new ActionState { IsChecked = allAreFirstLastPiecePrio }; + } + else + { + firstLastPiecePrio.IsChecked = allAreFirstLastPiecePrio; + } + + actionStates["superSeeding"] = ActionState.Hidden; + } + + if (allArePaused) + { + actionStates["pause"] = ActionState.Hidden; + } + else if (allAreForceStart) + { + actionStates["forceStart"] = ActionState.Hidden; + } + else if (!thereArePaused && !thereAreForceStart) + { + actionStates["start"] = ActionState.Hidden; + } + + if (!allAreAutoTmm && thereAreAutoTmm) + { + actionStates["autoTorrentManagement"] = ActionState.Hidden; + } + else + { + actionStates["autoTorrentManagement"] = new ActionState { IsChecked = allAreAutoTmm }; + } + + return Filter(actionStates); + } + + private IEnumerable Filter(Dictionary actionStates) + { + foreach (var action in _actions) + { + if (!actionStates.TryGetValue(action.Name, out var actionState)) + { + yield return action; + } + else + { + + if (actionState.Show is null || actionState.Show.Value) + { + var act = action with { }; + if (actionState.HasSeparator.HasValue) + { + act.SeparatorBefore = actionState.HasSeparator.Value; + } + if (actionState.IsChecked.HasValue) + { + act.IsChecked = actionState.IsChecked.Value; + } + } + } + } + } + + private sealed class ActionState + { + public bool? Show { get; set; } + + public bool? HasSeparator { get; set; } + + public bool? IsChecked { get; set; } + + public static readonly ActionState Hidden = new ActionState { Show = false }; + + public static readonly ActionState HasSeperator = new ActionState { HasSeparator = true }; } private EventCallback CreateCallback(Func action, bool ignoreAfterAction = false) @@ -351,46 +565,53 @@ namespace Lantean.QBTMudBlade.Components Children, } - public class Divider : TorrentAction + public record TorrentAction { - public Divider() : base("-", default!, Color.Default, default(EventCallback)) - { - } - } + private readonly Color _color; - public class TorrentAction - { - public TorrentAction(string name, string? icon, Color color, EventCallback callback) + public TorrentAction(string name, string text, string? icon, Color color, EventCallback callback, bool separatorBefore = false) { Name = name; + Text = text; Icon = icon; - Color = color; + _color = color; Callback = callback; + SeparatorBefore = separatorBefore; Children = []; } - public TorrentAction(string name, string? icon, Color color, IEnumerable children, bool multiAction = false, bool useTextButton = false) + public TorrentAction(string name, string text, string? icon, Color color, IEnumerable children, bool multiAction = false, bool useTextButton = false, bool separatorBefore = false) { Name = name; + Text = text; Icon = icon; - Color = color; + _color = color; Callback = default; Children = children; UseTextButton = useTextButton; + SeparatorBefore = separatorBefore; } public string Name { get; } + public string Text { get; } + public string? Icon { get; } - public Color Color { get; } + + + public Color Color => IsChecked is null || IsChecked.Value ? _color : Color.Transparent; public EventCallback Callback { get; } + public bool SeparatorBefore { get; set; } + public IEnumerable Children { get; } public bool UseTextButton { get; } public bool MultiAction { get; } + + public bool? IsChecked { get; internal set; } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/DialogHelper.cs b/Lantean.QBTMudBlade/DialogHelper.cs index 000979c..83afb2a 100644 --- a/Lantean.QBTMudBlade/DialogHelper.cs +++ b/Lantean.QBTMudBlade/DialogHelper.cs @@ -194,6 +194,25 @@ namespace Lantean.QBTMudBlade await onSuccess((T)dialogResult.Data); } + public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable hashes) + { + var parameters = new DialogParameters + { + { nameof(SliderFieldDialog.Value), rate }, + { nameof(SliderFieldDialog.Min), 0L }, + { nameof(SliderFieldDialog.Max), 100L }, + }; + var result = await dialogService.ShowAsync>("Download Rate", parameters, FormDialogOptions); + + var dialogResult = await result.Result; + if (dialogResult.Canceled) + { + return; + } + + await apiClient.SetTorrentDownloadLimit((long)dialogResult.Data, null, hashes.ToArray()); + } + public static async Task InvokeUploadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable hashes) { var parameters = new DialogParameters @@ -282,7 +301,7 @@ namespace Lantean.QBTMudBlade { nameof(SubMenuDialog.Preferences), preferences }, }; - await dialogService.ShowAsync(parent.Name, parameters, FormDialogOptions); + await dialogService.ShowAsync(parent.Text, parameters, FormDialogOptions); } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Extensions.cs b/Lantean.QBTMudBlade/Extensions.cs index 189700d..e5b3932 100644 --- a/Lantean.QBTMudBlade/Extensions.cs +++ b/Lantean.QBTMudBlade/Extensions.cs @@ -49,5 +49,15 @@ namespace Lantean.QBTMudBlade // disposed } } + + public static bool IsFinished(this Torrent torrent) + { + return torrent.TotalSize == torrent.Downloaded; + } + + public static bool MetaDownloaded(this Torrent torrent) + { + return !(torrent.State == "metaDL" || torrent.State == "forcedMetaDL" || torrent.TotalSize == -1); + } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/ContentItem.cs b/Lantean.QBTMudBlade/Models/ContentItem.cs index dc44555..6dea3ef 100644 --- a/Lantean.QBTMudBlade/Models/ContentItem.cs +++ b/Lantean.QBTMudBlade/Models/ContentItem.cs @@ -45,7 +45,7 @@ namespace Lantean.QBTMudBlade.Models public long Downloaded => (long)Math.Round(Size * Progress, 0); - public long Remaining => Progress == 1 ? 0 : Size - Downloaded; + public long Remaining => Progress == 1 || Priority == Priority.DoNotDownload ? 0 : Size - Downloaded; public bool IsFolder { get; } diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs index c619ec4..82fc4a3 100644 --- a/Lantean.QBTMudBlade/Pages/Login.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -47,12 +47,10 @@ namespace Lantean.QBTMudBlade.Pages } #if DEBUG - protected override async Task OnInitializedAsync() { - await DoLogin("admin", "23mIDZhvT"); + await DoLogin("admin", "MX6r8xzTP"); } - #endif } diff --git a/Lantean.QBTMudBlade/readme.md b/Lantean.QBTMudBlade/readme.md new file mode 100644 index 0000000..5ba4be6 --- /dev/null +++ b/Lantean.QBTMudBlade/readme.md @@ -0,0 +1,9 @@ +# qbt-mud + +## To-Do + +- Files -> Context menu alternative +- Details -> Fixed height +- Rename multiple files dialog +- RSS feeds and dialogs +- Search and dialogs \ No newline at end of file