Add context menu and fixes

This commit is contained in:
ahjephson
2024-07-18 18:52:03 +01:00
parent 50fbbd9e6e
commit ab548bb0e8
38 changed files with 994 additions and 104 deletions

View File

@@ -0,0 +1,23 @@
@inherits MudMenu
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
<MudPopover tracker="@Id"
Open="@_open"
Class="@PopoverClass"
MaxHeight="@MaxHeight"
AnchorOrigin="@AnchorOrigin"
TransformOrigin="TransformOrigin"
RelativeWidth="@FullWidth"
OverflowBehavior="OverflowBehavior.FlipAlways"
Style="@_popoverStyle"
@ontouchend:preventDefault>
<CascadingValue Value="@((MudMenu)this)">
<MudList T="object"
Class="@ListClass"
Dense="@Dense">
@ChildContent
</MudList>
</CascadingValue>
</MudPopover>
<MudOverlay Visible="@(_open)" OnClick="@ToggleMenuAsync" LockScroll="@LockScroll" />

View File

@@ -0,0 +1,232 @@
using Lantean.QBTMudBlade.Interop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using MudBlazor;
using MudBlazor.Utilities;
namespace Lantean.QBTMudBlade.Components
{
// This is a very hacky approach but works for now.
// This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked.
// MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't.
// Ideally this should be changed to use something like the way the DialogService works.
public partial class ContextMenu : MudMenu
{
private const double _diff = 64;
private bool _open;
private string? _popoverStyle;
private string? _id;
private double _x;
private double _y;
private bool _isResized = false;
private const double _drawerWidth = 235;
private string Id
{
get
{
_id ??= Guid.NewGuid().ToString();
return _id;
}
}
[Inject]
public IJSRuntime JSRuntime { get; set; } = default!;
[Inject]
public IPopoverService PopoverService { get; set; } = default!;
[CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; }
[Parameter]
public bool InsideDrawer { get; set; }
public new string? Label { get; }
public new string? AriaLabel { get; }
public new string? Icon { get; }
public new Color IconColor { get; } = Color.Inherit;
public new string? StartIcon { get; }
public new string? EndIcon { get; }
public new Color Color { get; } = Color.Default;
public new Size Size { get; } = Size.Medium;
public new Variant Variant { get; } = Variant.Text;
public new bool PositionAtCursor { get; } = true;
public new RenderFragment? ActivatorContent { get; } = null;
public new MouseEvent ActivationEvent { get; } = MouseEvent.LeftClick;
public new string? ListClass { get; } = "unselectable";
public new string? PopoverClass { get; } = "unselectable";
public ContextMenu()
{
AnchorOrigin = Origin.TopLeft;
TransformOrigin = Origin.TopLeft;
}
/// <summary>
/// Closes the menu.
/// </summary>
public new Task CloseMenuAsync()
{
_open = false;
_popoverStyle = null;
StateHasChanged();
return OpenChanged.InvokeAsync(_open);
}
/// <summary>
/// Opens the menu.
/// </summary>
/// <param name="args">
/// The arguments of the calling mouse/pointer event.
/// If <see cref="PositionAtCursor"/> is true, the menu will be positioned using the coordinates in this parameter.
/// </param>
public new async Task OpenMenuAsync(EventArgs args)
{
if (Disabled)
{
return;
}
// long press on iOS triggers selection, so clear it
await JSRuntime.ClearSelection();
_open = true;
_isResized = false;
StateHasChanged();
var (x, y) = GetPositionFromArgs(args);
_x = x;
_y = y;
SetPopoverStyle(x, y);
StateHasChanged();
await OpenChanged.InvokeAsync(_open);
}
/// <summary>
/// Sets the popover style ONLY when there is an activator.
/// </summary>
private void SetPopoverStyle(double x, double y)
{
_popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};";
}
/// <summary>
/// Toggle the visibility of the menu.
/// </summary>
public new async Task ToggleMenuAsync(EventArgs args)
{
if (Disabled)
{
return;
}
if (_open)
{
await CloseMenuAsync();
}
else
{
await OpenMenuAsync(args);
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_isResized)
{
await DeterminePosition();
}
}
private async Task DeterminePosition()
{
var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
double? contextMenuHeight = null;
double? contextMenuWidth = null;
var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
if (popoverSize.Height > 0)
{
contextMenuHeight = popoverSize.Height;
contextMenuWidth = popoverSize.Width;
}
else
{
return;
}
// the bottom position of the popover will be rendered off screen
if ((_y - _diff + contextMenuHeight.Value) >= (mainContentSize.Height))
{
// adjust the top of the context menu
var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
_y -= overshoot;
//if (_y < 70)
//{
// _y = 70;
//}
if ((_y - _diff + contextMenuHeight) >= mainContentSize.Height)
{
MaxHeight = (int)(mainContentSize.Height - _y + _diff);
}
}
if (_x + contextMenuWidth.Value > mainContentSize.Width)
{
var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
_x -= overshoot;
}
SetPopoverStyle(_x, _y);
_isResized = true;
await InvokeAsync(StateHasChanged);
}
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
{
double x, y;
if (eventArgs is MouseEventArgs mouseEventArgs)
{
x = mouseEventArgs.ClientX;
y = mouseEventArgs.ClientY;
}
else if (eventArgs is LongPressEventArgs longPressEventArgs)
{
x = longPressEventArgs.ClientX;
y = longPressEventArgs.ClientY;
}
else
{
throw new NotSupportedException("Invalid eventArgs type.");
}
return (x - (DrawerOpen && !InsideDrawer ? _drawerWidth : 0), y - (InsideDrawer ? _diff : 0));
}
}
}

View File

@@ -6,22 +6,28 @@ using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddCategoryDialog
public partial class CategoryPropertiesDialog
{
private string _savePath = string.Empty;
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
protected string? Category { get; set; }
[Parameter]
public string? Category { get; set; }
protected string SavePath { get; set; } = "";
[Parameter]
public string? SavePath { get; set; }
protected override async Task OnInitializedAsync()
{
var preferences = await ApiClient.GetApplicationPreferences();
SavePath = preferences.SavePath;
_savePath = preferences.SavePath;
SavePath ??= _savePath;
}
protected void Cancel(MouseEventArgs args)
@@ -35,6 +41,12 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
{
return;
}
if (string.IsNullOrEmpty(SavePath))
{
SavePath = _savePath;
}
MudDialog.Close(DialogResult.Ok(new Category(Category, SavePath)));
}
}

View File

@@ -4,7 +4,7 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField T="T" Label="@Label" Value="@Value" />
<MudTextField T="T" Label="@GetLabel()" Value="@Value" Disabled="Disabled" Variant="Variant.Outlined" ShrinkLabel="true" />
</MudItem>
</MudGrid>
</DialogContent>

View File

@@ -10,11 +10,23 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Label { get; set; } = default!;
public string? Label { get; set; }
[Parameter]
public T? Value { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public Func<T?, string?>? LabelFunc { get; set; }
private string? GetLabel()
{
var label = LabelFunc?.Invoke(Value);
return label is null ? Label : label;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();

View File

@@ -4,7 +4,7 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudNumericField T="T" Label="@Label" Value="@Value" Min="@Min" Max="@Max" Disabled="Disabled" />
<MudNumericField T="T" Label="@GetLabel()" Value="@Value" Min="@Min" Max="@Max" Disabled="Disabled" Variant="Variant.Outlined" ShrinkLabel="true" />
</MudItem>
<MudItem xs="12">
<MudSlider T="T" ValueLabel="true" Value="@Value" Min="@Min" Max="@Max" Disabled="Disabled" />

View File

@@ -11,7 +11,7 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Label { get; set; } = default!;
public string? Label { get; set; }
[Parameter]
public T Value { get; set; }
@@ -25,6 +25,15 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public Func<T, string?>? LabelFunc { get; set; }
private string? GetLabel()
{
var label = LabelFunc?.Invoke(Value);
return label is null ? Label : label;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();

View File

@@ -2,10 +2,8 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<CascadingValue Value="MainData">
<CascadingValue Value="Preferences">
<TorrentActions Hashes="Hashes" ParentAction="ParentAction" RenderType="RenderType.Children" MudDialog="MudDialog" />
</CascadingValue>
<TorrentActions Hashes="Hashes" ParentAction="ParentAction" RenderType="RenderType.Children" MudDialog="MudDialog" Torrents="Torrents" Preferences="Preferences" />
</CascadingValue>
</MudItem>
</MudGrid>

View File

@@ -13,7 +13,7 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
public TorrentAction? ParentAction { get; set; }
[Parameter]
public MainData MainData { get; set; } = default!;
public Dictionary<string, Torrent> Torrents { get; set; } = default!;
[Parameter]
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }

View File

@@ -0,0 +1,11 @@
<MudDialog ContentStyle="mix-width: 400px">
<DialogContent>
<MudGrid>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,53 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class TorrentOptionsDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
[EditorRequired]
public string Hash { get; set; } = default!;
[CascadingParameter]
public MainData MainData { get; set; } = default!;
[CascadingParameter]
public QBitTorrentClient.Models.Preferences Preferences { get; set; } = default!;
protected bool AutomaticTorrentManagement { get; set; }
protected string? SavePath { get; set; }
protected string? TempPath { get; set; }
protected override void OnInitialized()
{
if (!MainData.Torrents.TryGetValue(Hash, out var torrent))
{
return;
}
var tempPath = Preferences.TempPath;
AutomaticTorrentManagement = torrent.AutomaticTorrentManagement;
SavePath = torrent.SavePath;
TempPath = tempPath;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close();
}
}
}

View File

@@ -1,6 +1,5 @@
<div class="@Classname">
<div @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)"
class="@LinkClassname">
<div @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
@if (!string.IsNullOrEmpty(Icon))
{
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />

View File

@@ -37,9 +37,18 @@ namespace Lantean.QBTMudBlade.Components
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter]
public EventCallback<LongPressEventArgs> OnLongPress { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnContextMenu { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public RenderFragment? ContextMenu { get; set; }
protected string Classname =>
new CssBuilder("mud-nav-item")
@@ -67,5 +76,15 @@ namespace Lantean.QBTMudBlade.Components
await OnClick.InvokeAsync(ev);
}
protected Task OnLongPressInternal(LongPressEventArgs e)
{
return OnLongPress.InvokeAsync(e);
}
protected Task OnContextMenuInternal(MouseEventArgs e)
{
return OnContextMenu.InvokeAsync(e);
}
}
}

View File

@@ -1,27 +1,73 @@
<MudNavMenu Dense="true">
<ContextMenu @ref="StatusContextMenu" Dense="true" InsideDrawer="true">
@TorrentControls(_statusType)
</ContextMenu>
<ContextMenu @ref="CategoryContextMenu" Dense="true" InsideDrawer="true">
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
@if (IsCategoryTarget)
{
<MudMenuItem Icon="@Icons.Material.Filled.Edit" IconColor="Color.Info" OnClick="EditCategory">Edit category</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveCategory">Remove category</MudMenuItem>
}
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
<MudDivider />
@TorrentControls(_categoryType)
</ContextMenu>
<ContextMenu @ref="TagContextMenu" Dense="true" InsideDrawer="true">
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
@if (IsTagTarget)
{
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTag">Remove tag</MudMenuItem>
}
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
<MudDivider />
@TorrentControls(_tagType)
</ContextMenu>
<ContextMenu @ref="TrackerContextMenu" Dense="true" InsideDrawer="true">
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
<MudDivider />
@TorrentControls(_trackerType)
</ContextMenu>
<MudNavMenu Dense="true">
<MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
@foreach (var (status, count) in Statuses)
{
var (icon, color) = DisplayHelpers.GetStatusIcon(status);
<FakeNavLink Class="filter-menu-item" Active="@(Status == status)" Icon="@icon" IconColor="@color" OnClick="@(v => StatusValueChanged(status))">@($"{status.GetStatusName()} ({count})")</FakeNavLink>
<FakeNavLink Class="filter-menu-item" Active="@(Status == status)" Icon="@icon" IconColor="@color" OnClick="@(e => StatusValueChanged(status))" OnContextMenu="@(e => StatusOnContextMenu(e, status))" OnLongPress="@(e => StatusOnLongPress(e, status))">@($"{status.GetStatusName()} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Categories" @bind-Expanded="_categoriesExpanded">
@foreach (var (category, count) in Categories)
{
<FakeNavLink Class="filter-menu-item" Active="@(Category == category)" Icon="@Icons.Material.Filled.List" IconColor="Color.Info" OnClick="@(v => CategoryValueChanged(category))">@($"{category} ({count})")</FakeNavLink>
<FakeNavLink Class="filter-menu-item" Active="@(Category == category)" Icon="@Icons.Material.Filled.List" IconColor="Color.Info" OnClick="@(e => CategoryValueChanged(category))" OnContextMenu="@(e => CategoryOnContextMenu(e, category))" OnLongPress="@(e => CategoryOnLongPress(e, category))">@($"{category} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Tags" @bind-Expanded="_tagsExpanded">
@foreach (var (tag, count) in Tags)
{
<FakeNavLink Class="filter-menu-item" Active="@(Tag == tag)" Icon="@Icons.Material.Filled.Label" IconColor="Color.Info" OnClick="@(v => TagValueChanged(tag))">@($"{tag} ({count})")</FakeNavLink>
<FakeNavLink Class="filter-menu-item" Active="@(Tag == tag)" Icon="@Icons.Material.Filled.Label" IconColor="Color.Info" OnClick="@(e => TagValueChanged(tag))" OnContextMenu="@(e => TagOnContextMenu(e, tag))" OnLongPress="@(e => TagOnLongPress(e, tag))">@($"{tag} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Trackers" @bind-Expanded="_trackersExpanded">
@foreach (var (tracker, count) in Trackers)
{
<FakeNavLink Class="filter-menu-item" Active="@(Tracker == tracker)" Icon="@Icons.Material.Filled.PinDrop" IconColor="Color.Info" OnClick="@(v => TrackerValueChanged(tracker))">@($"{GetHostName(tracker)} ({count})")</FakeNavLink>
<FakeNavLink Class="filter-menu-item" Active="@(Tracker == tracker)" Icon="@Icons.Material.Filled.PinDrop" IconColor="Color.Info" OnClick="@(e => TrackerValueChanged(tracker))" OnContextMenu="@(e => TrackerOnContextMenu(e, tracker))" OnLongPress="@(e => TrackerOnLongPress(e, tracker))">@($"{tracker} ({count})")</FakeNavLink>
}
</MudNavGroup>
</MudNavMenu>
@code {
private RenderFragment TorrentControls(string type)
{
return __builder =>
{
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
};
}
}

View File

@@ -1,6 +1,10 @@
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Collections.Generic;
namespace Lantean.QBTMudBlade.Components
{
@@ -11,6 +15,11 @@ namespace Lantean.QBTMudBlade.Components
private const string _tagSelectionStorageKey = "FiltersNav.Selection.Tag";
private const string _trackerSelectionStorageKey = "FiltersNav.Selection.Tracker";
private const string _statusType = nameof(_statusType);
private const string _categoryType = nameof(_categoryType);
private const string _tagType = nameof(_tagType);
private const string _trackerType = nameof(_trackerType);
private bool _statusExpanded = true;
private bool _categoriesExpanded = true;
private bool _tagsExpanded = true;
@@ -27,9 +36,18 @@ namespace Lantean.QBTMudBlade.Components
[Inject]
public ILocalStorageService LocalStorage { get; set; } = default!;
[Inject]
public IDialogService DialogService { get; set; } = default!;
[Inject]
public IApiClient ApiClient { get; set; } = default!;
[CascadingParameter]
public MainData? MainData { get; set; }
[CascadingParameter]
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[Parameter]
public EventCallback<string> CategoryChanged { get; set; }
@@ -42,13 +60,33 @@ namespace Lantean.QBTMudBlade.Components
[Parameter]
public EventCallback<string> TrackerChanged { get; set; }
public Dictionary<string, int> Tags => MainData?.TagState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
protected Dictionary<string, int> Tags => GetTags();
public Dictionary<string, int> Categories => MainData?.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
protected Dictionary<string, int> Categories => GetCategories();
public Dictionary<string, int> Trackers => MainData?.TrackersState.GroupBy(d => GetHostName(d.Key)).Select(l => new KeyValuePair<string, int>(GetHostName(l.First().Key), l.Sum(i => i.Value.Count))).ToDictionary(d => d.Key, d => d.Value) ?? [];
protected Dictionary<string, int> Trackers => GetTrackers();
public Dictionary<string, int> Statuses => MainData?.StatusState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
protected Dictionary<string, int> Statuses => GetStatuses();
protected ContextMenu? StatusContextMenu { get; set; }
protected ContextMenu? CategoryContextMenu { get; set; }
protected ContextMenu? TagContextMenu { get; set; }
protected ContextMenu? TrackerContextMenu { get; set; }
protected string? ContextMenuStatus { get; set; }
protected bool IsCategoryTarget { get; set; }
protected string? ContextMenuCategory { get; set; }
protected bool IsTagTarget { get; set; }
protected string? ContextMenuTag { get; set; }
protected string? ContextMenuTracker { get; set; }
protected override async Task OnInitializedAsync()
{
@@ -96,6 +134,28 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected Task StatusOnContextMenu(MouseEventArgs args, string value)
{
return ShowStatusContextMenu(args, value);
}
protected Task StatusOnLongPress(LongPressEventArgs args, string value)
{
return ShowStatusContextMenu(args, value);
}
protected Task ShowStatusContextMenu(EventArgs args, string value)
{
if (StatusContextMenu is null)
{
return Task.CompletedTask;
}
ContextMenuStatus = value;
return StatusContextMenu.OpenMenuAsync(args);
}
protected async Task CategoryValueChanged(string value)
{
Category = value;
@@ -111,6 +171,29 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected Task CategoryOnContextMenu(MouseEventArgs args, string value)
{
return ShowCategoryContextMenu(args, value);
}
protected Task CategoryOnLongPress(LongPressEventArgs args, string value)
{
return ShowCategoryContextMenu(args, value);
}
protected Task ShowCategoryContextMenu(EventArgs args, string value)
{
if (CategoryContextMenu is null)
{
return Task.CompletedTask;
}
IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
ContextMenuCategory = value;
return CategoryContextMenu.OpenMenuAsync(args);
}
protected async Task TagValueChanged(string value)
{
Tag = value;
@@ -126,6 +209,29 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected Task TagOnContextMenu(MouseEventArgs args, string value)
{
return ShowTagContextMenu(args, value);
}
protected Task TagOnLongPress(LongPressEventArgs args, string value)
{
return ShowTagContextMenu(args, value);
}
protected Task ShowTagContextMenu(EventArgs args, string value)
{
if (TagContextMenu is null)
{
return Task.CompletedTask;
}
IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
ContextMenuTag = value;
return TagContextMenu.OpenMenuAsync(args);
}
protected async Task TrackerValueChanged(string value)
{
Tracker = value;
@@ -141,7 +247,210 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected static string GetHostName(string tracker)
protected Task TrackerOnContextMenu(MouseEventArgs args, string value)
{
return ShowTrackerContextMenu(args, value);
}
protected Task TrackerOnLongPress(LongPressEventArgs args, string value)
{
return ShowTrackerContextMenu(args, value);
}
protected Task ShowTrackerContextMenu(EventArgs args, string value)
{
if (TrackerContextMenu is null)
{
return Task.CompletedTask;
}
ContextMenuTracker = value;
return TrackerContextMenu.OpenMenuAsync(args);
}
protected async Task AddCategory()
{
await DialogService.ShowAddCategoryDialog(ApiClient);
}
protected async Task EditCategory()
{
if (ContextMenuCategory is null)
{
return;
}
await DialogService.ShowEditCategoryDialog(ApiClient, ContextMenuCategory);
}
protected async Task RemoveCategory()
{
if (ContextMenuCategory is null)
{
return;
}
await ApiClient.RemoveCategories(ContextMenuCategory);
Categories.Remove(ContextMenuCategory);
}
protected async Task RemoveUnusedCategories()
{
var removedCategories = await ApiClient.RemoveUnusedCategories();
foreach (var removedCategory in removedCategories)
{
Categories.Remove(removedCategory);
}
}
protected async Task AddTag()
{
if (ContextMenuTag is null)
{
return;
}
await DialogService.ShowAddTagsDialog(ApiClient);
}
protected async Task RemoveTag()
{
if (ContextMenuTag is null)
{
return;
}
await ApiClient.DeleteTags(ContextMenuTag);
Tags.Remove(ContextMenuTag);
}
protected async Task RemoveUnusedTags()
{
var removedTags = await ApiClient.RemoveUnusedTags();
foreach (var removedTag in removedTags)
{
Tags.Remove(removedTag);
}
}
protected async Task ResumeTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.ResumeTorrents(torrents);
}
protected async Task PauseTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.PauseTorrents(torrents);
}
protected async Task RemoveTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
}
private Dictionary<string, int> GetTags()
{
if (MainData is null)
{
return [];
}
return MainData.TagState.ToDictionary(d => d.Key, d => d.Value.Count);
}
private Dictionary<string, int> GetCategories()
{
if (MainData is null)
{
return [];
}
return MainData.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count);
}
private Dictionary<string, int> GetTrackers()
{
if (MainData is null)
{
return [];
}
return MainData.TrackersState
.GroupBy(d => GetHostName(d.Key))
.Select(l => new KeyValuePair<string, int>(GetHostName(l.First().Key), l.Sum(i => i.Value.Count)))
.ToDictionary(d => d.Key, d => d.Value);
}
private Dictionary<string, int> GetStatuses()
{
if (MainData is null)
{
return [];
}
return MainData.StatusState.ToDictionary(d => d.Key, d => d.Value.Count);
}
private List<string> GetAffectedTorrentHashes(string type)
{
if (MainData is null)
{
return [];
}
switch (type)
{
case _statusType:
if (ContextMenuStatus is null)
{
return [];
}
var status = Enum.Parse<Status>(ContextMenuStatus);
return MainData.Torrents.Where(t => FilterHelper.FilterStatus(t.Value, status)).Select(t => t.Value.Hash).ToList();
case _categoryType:
if (ContextMenuCategory is null)
{
return [];
}
return MainData.Torrents.Where(t => FilterHelper.FilterCategory(t.Value, ContextMenuCategory, Preferences?.UseSubcategories ?? false)).Select(t => t.Value.Hash).ToList();
case _tagType:
if (ContextMenuTag is null)
{
return [];
}
return MainData.Torrents.Where(t => FilterHelper.FilterTag(t.Value, ContextMenuTag)).Select(t => t.Value.Hash).ToList();
case _trackerType:
if (ContextMenuTracker is null)
{
return [];
}
return MainData.Torrents.Where(t => FilterHelper.FilterTracker(t.Value, ContextMenuTracker)).Select(t => t.Value.Hash).ToList();
default:
return [];
}
}
private static string GetHostName(string tracker)
{
try
{

View File

@@ -1,7 +1,6 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using MudBlazor.Utilities;
namespace Lantean.QBTMudBlade.Components
{

View File

@@ -73,6 +73,10 @@ else if (RenderType == RenderType.MenuWithoutActivator)
<div style="width: 100%; height: 100%" @oncontextmenu="@(e => OverlayVisible = false)" @oncontextmenu:preventDefault></div>
</MudOverlay>
}
else if (RenderType == RenderType.MenuItems)
{
@MenuContents(Actions)
}
@code {
private RenderFragment ToolbarContent

View File

@@ -44,10 +44,10 @@ namespace Lantean.QBTMudBlade.Components
[Parameter]
public RenderType RenderType { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
[Parameter, EditorRequired]
public Dictionary<string, Torrent> Torrents { get; set; } = default!;
[CascadingParameter]
[Parameter, EditorRequired]
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[Parameter]
@@ -155,7 +155,7 @@ namespace Lantean.QBTMudBlade.Components
protected async Task SetLocation()
{
string? savePath = null;
if (Hashes.Any() && MainData.Torrents.TryGetValue(Hashes.First(), out var torrent))
if (Hashes.Any() && Torrents.TryGetValue(Hashes.First(), out var torrent))
{
savePath = torrent.SavePath;
}
@@ -167,7 +167,7 @@ namespace Lantean.QBTMudBlade.Components
{
string? name = null;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
if (Hashes.Any() && Torrents.TryGetValue(hash, out var torrent))
{
name = torrent.Name;
}
@@ -196,7 +196,7 @@ namespace Lantean.QBTMudBlade.Components
{
long downloadLimit = -1;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
if (Hashes.Any() && Torrents.TryGetValue(hash, out var torrent))
{
downloadLimit = torrent.UploadLimit;
}
@@ -208,7 +208,7 @@ namespace Lantean.QBTMudBlade.Components
{
long uploadLimit = -1;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
if (Hashes.Any() && Torrents.TryGetValue(hash, out var torrent))
{
uploadLimit = torrent.UploadLimit;
}
@@ -220,7 +220,7 @@ namespace Lantean.QBTMudBlade.Components
{
float ratioLimit = -1;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
if (Hashes.Any() && Torrents.TryGetValue(hash, out var torrent))
{
ratioLimit = torrent.RatioLimit;
}
@@ -322,14 +322,14 @@ namespace Lantean.QBTMudBlade.Components
protected async Task SubMenuTouch(TorrentAction action)
{
await DialogService.ShowSubMenu(Hashes, action, MainData, Preferences);
await DialogService.ShowSubMenu(Hashes, action, Torrents, Preferences);
}
private IEnumerable<Torrent> GetTorrents()
{
foreach (var hash in Hashes)
{
if (MainData.Torrents.TryGetValue(hash, out var torrent))
if (Torrents.TryGetValue(hash, out var torrent))
{
yield return torrent;
}
@@ -602,6 +602,8 @@ namespace Lantean.QBTMudBlade.Components
Children,
MenuWithoutActivator,
MenuItems,
}
public record TorrentAction

View File

@@ -120,7 +120,7 @@ namespace Lantean.QBTMudBlade
public static async Task<string?> ShowAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient)
{
var reference = await dialogService.ShowAsync<AddCategoryDialog>("New Category", NonBlurFormDialogOptions);
var reference = await dialogService.ShowAsync<CategoryPropertiesDialog>("New Category", NonBlurFormDialogOptions);
var dialogResult = await reference.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{
@@ -134,6 +134,29 @@ namespace Lantean.QBTMudBlade
return category.Name;
}
public static async Task<string?> ShowEditCategoryDialog(this IDialogService dialogService, IApiClient apiClient, string categoryName)
{
var category = (await apiClient.GetAllCategories()).FirstOrDefault(c => c.Key == categoryName).Value;
var parameters = new DialogParameters
{
{ nameof(CategoryPropertiesDialog.Category), category?.Name },
{ nameof(CategoryPropertiesDialog.SavePath), category?.SavePath },
};
var reference = await dialogService.ShowAsync<CategoryPropertiesDialog>("Edit Category", parameters, NonBlurFormDialogOptions);
var dialogResult = await reference.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{
return null;
}
var updatedCategory = (Category)dialogResult.Data;
await apiClient.EditCategory(updatedCategory.Name, updatedCategory.SavePath);
return updatedCategory.Name;
}
public static async Task<HashSet<string>?> ShowAddTagsDialog(this IDialogService dialogService, IApiClient apiClient)
{
var reference = await dialogService.ShowAsync<AddTagDialog>("Add Tags", NonBlurFormDialogOptions);
@@ -208,11 +231,14 @@ namespace Lantean.QBTMudBlade
public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
{
Func<long, string> labelFunc = v => v == Limits.NoLimit ? "No limit" : v.ToString();
var parameters = new DialogParameters
{
{ nameof(SliderFieldDialog<long>.Value), rate },
{ nameof(SliderFieldDialog<long>.Min), 0L },
{ nameof(SliderFieldDialog<long>.Max), 100L },
{ nameof(SingleFieldDialog<long>.LabelFunc), labelFunc }
};
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Download Rate", parameters, FormDialogOptions);
@@ -232,7 +258,7 @@ namespace Lantean.QBTMudBlade
{ nameof(SliderFieldDialog<long>.Value), rate },
{ nameof(SliderFieldDialog<long>.Min), 0L },
{ nameof(SliderFieldDialog<long>.Max), 100L },
{ nameof(SliderFieldDialog<long>.Disabled), rate == -2 },
{ nameof(SliderFieldDialog<long>.Disabled), rate == Limits.GlobalLimit },
};
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions);
@@ -306,13 +332,13 @@ namespace Lantean.QBTMudBlade
await Task.Delay(0);
}
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, TorrentAction parent, MainData mainData, QBitTorrentClient.Models.Preferences? preferences)
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, TorrentAction parent, Dictionary<string, Torrent> torrents, QBitTorrentClient.Models.Preferences? preferences)
{
var parameters = new DialogParameters
{
{ nameof(SubMenuDialog.ParentAction), parent },
{ nameof(SubMenuDialog.Hashes), hashes },
{ nameof(SubMenuDialog.MainData), mainData },
{ nameof(SubMenuDialog.Torrents), torrents },
{ nameof(SubMenuDialog.Preferences), preferences },
};

View File

@@ -2,16 +2,16 @@
{
public class BoundingClientRect : ClientSize
{
public int Bottom { get; set; }
public double Bottom { get; set; }
public int Top { get; set; }
public double Top { get; set; }
public int Left { get; set; }
public double Left { get; set; }
public int Right { get; set; }
public double Right { get; set; }
public int X { get; set; }
public double X { get; set; }
public int Y { get; set; }
public double Y { get; set; }
}
}

View File

@@ -2,8 +2,8 @@
{
public class ClientSize
{
public int Width { get; set; }
public double Width { get; set; }
public int Height { get; set; }
public double Height { get; set; }
}
}

View File

@@ -43,5 +43,10 @@ namespace Lantean.QBTMudBlade.Interop
{
await runtime.InvokeVoidAsync("qbt.copyTextToClipboard", value);
}
public static async Task ClearSelection(this IJSRuntime runtime)
{
await runtime.InvokeVoidAsync("qbt.clearSelection");
}
}
}

View File

@@ -31,4 +31,12 @@
</Content>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Components\ContextMenu.razor" />
</ItemGroup>
<ItemGroup>
<_ContentIncludedByDefault Remove="Components\ContextMenu.razor" />
</ItemGroup>
</Project>

View File

@@ -1,6 +1,6 @@
@inherits LayoutComponentBase
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
<MudDialogProvider />
@@ -30,12 +30,10 @@
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="Theme">
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</MudLayout>
</EnhancedErrorBoundary>
</CascadingValue>

View File

@@ -9,7 +9,7 @@
}
@if (Hash is not null)
{
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>

View File

@@ -17,7 +17,10 @@ namespace Lantean.QBTMudBlade.Pages
protected NavigationManager NavigationManager { get; set; } = default!;
[CascadingParameter]
public MainData? MainData { get; set; }
public MainData MainData { get; set; } = default!;
[CascadingParameter]
public QBitTorrentClient.Models.Preferences Preferences { get; set; } = default!;
[CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; }

View File

@@ -48,7 +48,7 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG
protected override Task OnInitializedAsync()
{
return DoLogin("admin", "XHkLRqsPq");
return DoLogin("admin", "42jMKTW7C");
}
#endif
}

View File

@@ -0,0 +1,26 @@
@page "/test"
@layout OtherLayout
<MudContainer Style="height: 700px;">
<ContextMenu @ref="ContextMenu">
<MudMenuItem>Test 1</MudMenuItem>
<MudDivider />
<MudMenuItem>Test 2</MudMenuItem>
</ContextMenu>
<MudButton OnClick="Click">Open</MudButton>
</MudContainer>
@code {
ContextMenu? ContextMenu;
protected async Task Click(MouseEventArgs args)
{
if (ContextMenu is null)
{
return;
}
await ContextMenu.OpenMenuAsync(args);
}
}

View File

@@ -5,7 +5,7 @@
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
<MudDivider Vertical="true" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrent" title="View torrent details" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
@@ -13,7 +13,11 @@
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
<TorrentActions @ref="ContextMenuActions" RenderType="RenderType.MenuWithoutActivator" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" />
@* <TorrentActions @ref="ContextMenuActions" RenderType="RenderType.MenuWithoutActivator" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" /> *@
<ContextMenu @ref="ContextMenu" Dense="true">
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
</ContextMenu>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable

View File

@@ -29,6 +29,9 @@ namespace Lantean.QBTMudBlade.Pages
[CascadingParameter]
public IEnumerable<Torrent>? Torrents { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
[CascadingParameter(Name = "SearchTermChanged")]
public EventCallback<string> SearchTermChanged { get; set; }
@@ -50,6 +53,8 @@ namespace Lantean.QBTMudBlade.Pages
protected Torrent? ContextMenuItem { get; set; }
protected ContextMenu? ContextMenu { get; set; }
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
{
SelectedItems = selectedItems;
@@ -131,26 +136,29 @@ namespace Lantean.QBTMudBlade.Pages
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<Torrent> eventArgs)
{
return ShowContextMenu(eventArgs.Item, eventArgs.MouseEventArgs.ClientX, eventArgs.MouseEventArgs.ClientY);
return ShowContextMenu(eventArgs.Item, eventArgs.MouseEventArgs);
//return ShowContextMenu(eventArgs.Item, eventArgs.MouseEventArgs.ClientX, eventArgs.MouseEventArgs.ClientY);
}
protected Task TableDataLongPress(TableDataLongPressEventArgs<Torrent> eventArgs)
{
return ShowContextMenu(eventArgs.Item, eventArgs.LongPressEventArgs.ClientX, eventArgs.LongPressEventArgs.ClientY);
return ShowContextMenu(eventArgs.Item, eventArgs.LongPressEventArgs);
//return ShowContextMenu(eventArgs.Item, eventArgs.LongPressEventArgs.ClientX, eventArgs.LongPressEventArgs.ClientY);
}
protected async Task ShowContextMenu(Torrent? torrent, double x, double y)
{
if (ContextMenuActions is null || ContextMenuActions.ActionsMenu is null)
{
return;
}
if (torrent is not null)
{
ContextMenuItem = torrent;
}
await JSRuntime.ClearSelection();
if (ContextMenuActions is null || ContextMenuActions.ActionsMenu is null)
{
return;
}
int? maxHeight = null;
var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
@@ -169,7 +177,7 @@ namespace Lantean.QBTMudBlade.Pages
if ((y - 64 + contextMenuHeight) >= mainContentSize.Height)
{
maxHeight = mainContentSize.Height - (int)y + 64;
maxHeight = (int)mainContentSize.Height - (int)y + 64;
}
}
@@ -187,6 +195,21 @@ namespace Lantean.QBTMudBlade.Pages
await ContextMenuActions.ActionsMenu.OpenMenuAsync(mouseEventArgs);
}
protected async Task ShowContextMenu(Torrent? torrent, EventArgs eventArgs)
{
if (torrent is not null)
{
ContextMenuItem = torrent;
}
if (ContextMenu is null)
{
return;
}
await ContextMenu.ToggleMenuAsync(eventArgs);
}
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =

View File

@@ -246,14 +246,26 @@ namespace Lantean.QBTMudBlade.Services
torrentList.TagState[FilterHelper.TAG_UNTAGGED].AddIfTrue(hash, FilterHelper.FilterTag(torrent, FilterHelper.TAG_UNTAGGED));
foreach (var tag in torrentList.Tags)
{
torrentList.TagState[tag].AddIfTrue(hash, FilterHelper.FilterTag(torrent, tag));
if (!torrentList.TagState.TryGetValue(tag, out HashSet<string>? value))
{
value = [];
torrentList.TagState.Add(tag, value);
}
value.AddIfTrue(hash, FilterHelper.FilterTag(torrent, tag));
}
torrentList.CategoriesState[FilterHelper.CATEGORY_ALL].Add(hash);
torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].AddIfTrue(hash, FilterHelper.FilterCategory(torrent, FilterHelper.CATEGORY_UNCATEGORIZED, torrentList.ServerState.UseSubcategories));
foreach (var category in torrentList.Categories.Keys)
{
torrentList.CategoriesState[category].AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
if (!torrentList.CategoriesState.TryGetValue(category, out HashSet<string>? value))
{
value = [];
torrentList.CategoriesState.Add(category, value);
}
value.AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
@@ -265,7 +277,12 @@ namespace Lantean.QBTMudBlade.Services
torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].AddIfTrue(hash, FilterHelper.FilterTracker(torrent, FilterHelper.TRACKER_TRACKERLESS));
foreach (var tracker in torrentList.Trackers.Keys)
{
torrentList.TrackersState[tracker].AddIfTrue(hash, FilterHelper.FilterTracker(torrent, tracker));
if (!torrentList.TrackersState.TryGetValue(tracker, out HashSet<string>? value))
{
value = [];
torrentList.TrackersState.Add(tracker, value);
}
value.AddIfTrue(hash, FilterHelper.FilterTracker(torrent, tracker));
}
}
@@ -624,7 +641,7 @@ namespace Lantean.QBTMudBlade.Services
var directoriesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && c.Value.IsFolder && c.Value.Level == level + 1).ToList();
var allContents = filesContents.Concat(directoriesContents);
var priorities = allContents.Select(d => d.Value.Priority).Distinct();
var downloadingContents = allContents.Where(c => c.Value.Priority != Priority.DoNotDownload).ToList();
var downloadingContents = allContents.Where(c => c.Value.Priority != Priority.DoNotDownload && !c.Value.IsFolder).ToList();
long size = 0;
float availability = 0;

View File

@@ -5,6 +5,14 @@
- Rename multiple files dialog
- RSS feeds and dialogs
- About
- Context Menu component
- Context menu for files list, filter (categories, tags & trackers)
- Tag management page
- Category management page
- Update all tables to use DynamicTable
- WebSeeds
- Trackers
- Peers
- Log
- Blocks
- Search

View File

@@ -13,7 +13,8 @@ window.qbt.triggerFileDownload = (url, fileName) => {
window.qbt.getBoundingClientRect = (selector) => {
const element = getElementBySelector(selector);
return element.getBoundingClientRect();
const rect = element.getBoundingClientRect();
return rect;
}
window.qbt.getInnerDimensions = (selector) => {
@@ -86,6 +87,21 @@ window.qbt.copyTextToClipboard = (text) => {
return navigator.clipboard.writeText(text);
}
window.qbt.clearSelection = () => {
if (window.getSelection) {
if (window.getSelection().empty) {
// Chrome
window.getSelection().empty();
} else if (window.getSelection().removeAllRanges) {
// Firefox
window.getSelection().removeAllRanges();
}
} else if (document.selection) {
// IE
document.selection.empty();
}
}
function fallbackCopyTextToClipboard(text) {
const textArea = document.createElement("textarea");
textArea.value = text;

View File

@@ -837,7 +837,7 @@ namespace Lantean.QBitTorrentClient
response.EnsureSuccessStatusCode();
}
public async Task DeleteTags(IEnumerable<string> tags)
public async Task DeleteTags(params string[] tags)
{
var content = new FormUrlEncodedBuilder()
.AddCommaSeparated("tags", tags)

View File

@@ -130,5 +130,33 @@ namespace Lantean.QBitTorrentClient
{
return apiClient.ReannounceTorrents(null, hash);
}
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
{
var torrents = await apiClient.GetTorrentList();
var categories = await apiClient.GetAllCategories();
var selectedCategories = torrents.Select(t => t.Category).Distinct().ToList();
var unusedCategories = categories.Values.Select(v => v.Name).Except(selectedCategories).Where(v => v is not null).Select(v => v!).ToArray();
await apiClient.RemoveCategories(unusedCategories);
return unusedCategories;
}
public static async Task<IEnumerable<string>> RemoveUnusedTags(this IApiClient apiClient)
{
var torrents = await apiClient.GetTorrentList();
var tags = await apiClient.GetAllTags();
var selectedTags = torrents.Where(t => t.Tags is not null).SelectMany(t => t.Tags!).Distinct().ToList();
var unusedTags = tags.Except(selectedTags).ToArray();
await apiClient.DeleteTags(unusedTags);
return unusedTags;
}
}
}

View File

@@ -150,7 +150,7 @@ namespace Lantean.QBitTorrentClient
Task CreateTags(IEnumerable<string> tags);
Task DeleteTags(IEnumerable<string> tags);
Task DeleteTags(params string[] tags);
Task SetAutomaticTorrentManagement(bool enable, bool? all = null, params string[] hashes);

View File

@@ -56,7 +56,7 @@ namespace Lantean.QBitTorrentClient
return _apiClient.DecreaseTorrentPriority(all, hashes);
}
public Task DeleteTags(IEnumerable<string> tags)
public Task DeleteTags(params string[] tags)
{
return _apiClient.DeleteTags(tags);
}