mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-23 04:52:22 +00:00
Add context menu and fixes
This commit is contained in:
23
Lantean.QBTMudBlade/Components/ContextMenu.razor
Normal file
23
Lantean.QBTMudBlade/Components/ContextMenu.razor
Normal 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" />
|
232
Lantean.QBTMudBlade/Components/ContextMenu.razor.cs
Normal file
232
Lantean.QBTMudBlade/Components/ContextMenu.razor.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
@@ -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)));
|
||||
}
|
||||
}
|
@@ -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>
|
||||
|
@@ -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();
|
||||
|
@@ -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" />
|
||||
|
@@ -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();
|
||||
|
@@ -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>
|
||||
|
@@ -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; }
|
||||
|
@@ -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>
|
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
@@ -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" />
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@@ -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>
|
||||
};
|
||||
}
|
||||
}
|
@@ -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
|
||||
{
|
||||
|
@@ -1,7 +1,6 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Utilities;
|
||||
|
||||
namespace Lantean.QBTMudBlade.Components
|
||||
{
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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 },
|
||||
};
|
||||
|
||||
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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; }
|
||||
}
|
||||
}
|
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
@@ -31,4 +31,12 @@
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<UpToDateCheckInput Remove="Components\ContextMenu.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<_ContentIncludedByDefault Remove="Components\ContextMenu.razor" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -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>
|
@@ -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>
|
||||
|
@@ -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; }
|
||||
|
@@ -48,7 +48,7 @@ namespace Lantean.QBTMudBlade.Pages
|
||||
#if DEBUG
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
return DoLogin("admin", "XHkLRqsPq");
|
||||
return DoLogin("admin", "42jMKTW7C");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
26
Lantean.QBTMudBlade/Pages/TestPage.razor
Normal file
26
Lantean.QBTMudBlade/Pages/TestPage.razor
Normal 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);
|
||||
}
|
||||
}
|
@@ -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
|
||||
|
@@ -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; } =
|
||||
|
@@ -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;
|
||||
|
@@ -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
|
@@ -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;
|
||||
|
@@ -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)
|
||||
|
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -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);
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
Reference in New Issue
Block a user