Update tag and category management and improve multiselection for DynamicTable

This commit is contained in:
ahjephson
2024-05-24 19:27:47 +01:00
parent 8cbdf4f2b6
commit b90cd7c90b
29 changed files with 634 additions and 157 deletions

View File

@@ -2,10 +2,10 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField Label="Category" @bind-Value="Category" />
<MudTextField Label="Category" @bind-Value="Category" Required ShrinkLabel Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save Path" @bind-Value="SavePath" />
<MudTextField Label="Save Path" @bind-Value="SavePath" Required ShrinkLabel Variant="Variant.Outlined" />
</MudItem>
</MudGrid>
</DialogContent>

View File

@@ -1,4 +1,5 @@
using Lantean.QBTMudBlade.Models;
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
@@ -10,10 +11,19 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
protected string? Category { get; set; }
protected string SavePath { get; set; } = "";
protected override async Task OnInitializedAsync()
{
var preferences = await ApiClient.GetApplicationPreferences();
SavePath = preferences.SavePath;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();

View File

@@ -0,0 +1,24 @@
<MudDialog>
<DialogContent>
<table width="100%">
<tbody>
<tr>
<td style="width: 100%"><MudTextField T="string" Label="Tag" Value="@Tag" ValueChanged="SetTag" ShrinkLabel Required Variant="Variant.Outlined" /></td>
<td><MudIconButton Icon="@Icons.Material.Filled.Add" OnClick="AddTag" /></td>
</tr>
@foreach (var tag in Tags)
{
var tagRef = tag;
<tr>
<td>@tag</td>
<td><MudIconButton Icon="@Icons.Material.Filled.Delete" /></td>
</tr>
}
</tbody>
</table>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,47 @@
using Lantean.QBitTorrentClient;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddTagDialog
{
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected HashSet<string> Tags { get; } = [];
protected string? Tag { get; set; }
protected void AddTag()
{
if (string.IsNullOrEmpty(Tag))
{
return;
}
Tags.Add(Tag);
Tag = null;
}
protected void SetTag(string tag)
{
Tag = tag;
}
protected void Cancel()
{
MudDialog.Cancel();
}
protected void Submit()
{
MudDialog.Close(Tags);
}
}
}

View File

@@ -0,0 +1,20 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudList Clickable="true">
<MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddCategory">Add</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveCategory">Remove</MudListItem>
<MudDivider />
@foreach (var category in Categories)
{
var categoryRef = category;
<MudListItem Icon="@GetIcon(categoryRef)" IconColor="Color.Default" OnClick="@(e => SetCategory(categoryRef))">@categoryRef</MudListItem>
}
</MudList>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,137 @@
using Lantean.QBitTorrentClient;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class ManageCategoriesDialog
{
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];
protected HashSet<string> Categories { get; set; } = [];
protected IList<string> TorrentCategories { get; private set; } = [];
protected override async Task OnInitializedAsync()
{
Categories = [.. (await ApiClient.GetAllCategories()).Select(c => c.Key)];
if (!Hashes.Any())
{
return;
}
await GetTorrentCategories();
}
private async Task GetTorrentCategories()
{
var torrents = await ApiClient.GetTorrentList(hashes: Hashes.ToArray());
TorrentCategories = torrents.Where(t => !string.IsNullOrEmpty(t.Category)).Select(t => t.Category!).ToList();
}
protected string GetIcon(string tag)
{
var state = GetCategoryState(tag);
return state switch
{
CategoryState.All => Icons.Material.Filled.RadioButtonChecked,
CategoryState.Partial => CustomIcons.RadioIndeterminate,
_ => Icons.Material.Filled.RadioButtonUnchecked
};
}
private enum CategoryState
{
All,
Partial,
None,
}
private CategoryState GetCategoryState(string category)
{
if (category == string.Empty || TorrentCategories.Count == 0)
{
return CategoryState.None;
}
if (TorrentCategories.All(c => c == category))
{
return CategoryState.All;
}
else if (TorrentCategories.Any(c => c == category))
{
return CategoryState.Partial;
}
else
{
return CategoryState.None;
}
}
protected async Task SetCategory(string category)
{
var state = GetCategoryState(category);
var nextState = state switch
{
CategoryState.All => CategoryState.None,
CategoryState.Partial => CategoryState.All,
CategoryState.None => CategoryState.All,
_ => CategoryState.None,
};
if (nextState == CategoryState.All)
{
await ApiClient.SetTorrentCategory(category, Hashes);
}
else
{
await ApiClient.RemoveTorrentCategory(Hashes);
}
await GetTorrentCategories();
await InvokeAsync(StateHasChanged);
}
protected async Task AddCategory()
{
var addedCategoy = await DialogService.ShowAddCategoryDialog(ApiClient);
if (addedCategoy is null)
{
return;
}
await ApiClient.SetTorrentCategory(addedCategoy, Hashes);
Categories.Add(addedCategoy);
await GetTorrentCategories();
}
protected async Task RemoveCategory()
{
await ApiClient.RemoveTorrentCategory(Hashes);
await GetTorrentCategories();
}
protected Task CloseDialog()
{
MudDialog.Close();
return Task.CompletedTask;
}
protected void Cancel()
{
MudDialog.Cancel();
}
}
}

View File

@@ -0,0 +1,20 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudList Clickable="true">
<MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddTag">Add</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveAllTags">Remove All</MudListItem>
<MudDivider />
@foreach (var tag in Tags)
{
var tagRef = tag;
<MudListItem Icon="@GetIcon(tagRef)" IconColor="Color.Default" OnClick="@(e => SetTag(tagRef))">@tag</MudListItem>
}
</MudList>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,139 @@
using Lantean.QBitTorrentClient;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using static MudBlazor.CategoryTypes;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class ManageTagsDialog
{
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];
protected HashSet<string> Tags { get; set; } = [];
protected IList<IReadOnlyList<string>> TorrentTags { get; private set; } = [];
protected override async Task OnInitializedAsync()
{
Tags = [.. (await ApiClient.GetAllTags())];
if (!Hashes.Any())
{
return;
}
await GetTorrentTags();
}
private async Task GetTorrentTags()
{
var torrents = await ApiClient.GetTorrentList(hashes: Hashes.ToArray());
TorrentTags = torrents.Select(t => t.Tags ?? []).ToList();
}
protected string GetIcon(string tag)
{
var state = GetTagState(tag);
return state switch
{
TagState.All => Icons.Material.Filled.CheckBox,
TagState.Partial => Icons.Material.Filled.IndeterminateCheckBox,
_ => Icons.Material.Filled.CheckBoxOutlineBlank
};
}
private enum TagState
{
All,
Partial,
None,
}
private TagState GetTagState(string tag)
{
if (TorrentTags.All(t => t.Contains(tag)))
{
return TagState.All;
}
else if (TorrentTags.Any(t => t.Contains(tag)))
{
return TagState.Partial;
}
else
{
return TagState.None;
}
}
protected async Task SetTag(string tag)
{
var state = GetTagState(tag);
var nextState = state switch
{
TagState.All => TagState.None,
TagState.Partial => TagState.All,
TagState.None => TagState.All,
_ => TagState.None,
};
if (nextState == TagState.All)
{
await ApiClient.AddTorrentTag(tag, Hashes);
}
else
{
await ApiClient.RemoveTorrentTag(tag, Hashes);
}
await GetTorrentTags();
await InvokeAsync(StateHasChanged);
}
protected async Task AddTag()
{
var addedTags = await DialogService.ShowAddTagsDialog(ApiClient);
if (addedTags is null || addedTags.Count == 0)
{
return;
}
await ApiClient.AddTorrentTags(addedTags, Hashes);
foreach (var tag in addedTags)
{
Tags.Add(tag);
}
await GetTorrentTags();
}
protected async Task RemoveAllTags()
{
await ApiClient.RemoveTorrentTags(Tags, Hashes);
await GetTorrentTags();
}
protected Task CloseDialog()
{
MudDialog.Close();
return Task.CompletedTask;
}
protected void Cancel()
{
MudDialog.Cancel();
}
}
}

View File

@@ -1,9 +1,19 @@
<MudDialog ContentStyle="mix-width: 400px">
<DialogContent>
<MudGrid>
<MudItem xl="12">
<TorrentActions Hashes="Hashes" ParentAction="ParentAction" RenderType="RenderType.Children" AfterAction="CloseDialog" />
<MudItem xs="12">
<CascadingValue Value="MainData">
<CascadingValue Value="Preferences">
<TorrentActions Hashes="Hashes" ParentAction="ParentAction" RenderType="RenderType.Children" AfterAction="CloseDialog" />
</CascadingValue>
</CascadingValue>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
@if (MultiAction)
{
<MudButton OnClick="Cancel">Close</MudButton>
}
</DialogActions>
</MudDialog>

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
@@ -11,6 +12,14 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[Parameter]
public TorrentAction? ParentAction { get; set; }
[Parameter]
public MainData MainData { get; set; } = default!;
[Parameter]
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
protected bool MultiAction => ParentAction?.MultiAction ?? false;
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];

View File

@@ -37,14 +37,15 @@
@foreach (var column in GetColumns())
{
var className = column.IconOnly ? null : "overflow-cell";
var columnHeader = column.IconOnly ? "" : column.Header;
<MudTh Class="@className" Style="@(GetColumnStyle(column))">
@if (column.SortSelector is not null)
{
<SortLabel SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@column.Header</SortLabel>
<SortLabel SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@columnHeader</SortLabel>
}
else
{
@column.Header
@columnHeader
}
</MudTh>
}

View File

@@ -4,6 +4,7 @@ using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Data.Common;
using static MudBlazor.CategoryTypes;
namespace Lantean.QBTMudBlade.Components
{
@@ -55,8 +56,8 @@ namespace Lantean.QBTMudBlade.Components
[Parameter]
public EventCallback<T> SelectedItemChanged { get; set; }
[Parameter]
public T? SelectedItem { get; set; }
//[Parameter]
//public T? SelectedItem { get; set; }
[Parameter]
public Func<ColumnDefinition<T>, bool> ColumnFilter { get; set; } = t => true;
@@ -193,16 +194,51 @@ namespace Lantean.QBTMudBlade.Components
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
{
SelectedItem = eventArgs.Item;
if (MultiSelection)
{
if (eventArgs.MouseEventArgs.CtrlKey)
{
if (SelectedItems.Contains(eventArgs.Item))
{
SelectedItems.Remove(eventArgs.Item);
}
else
{
SelectedItems.Add(eventArgs.Item);
}
}
else if (eventArgs.MouseEventArgs.AltKey)
{
SelectedItems.Clear();
SelectedItems.Add(eventArgs.Item);
}
else
{
if (!SelectedItems.Contains(eventArgs.Item))
{
SelectedItems.Clear();
SelectedItems.Add(eventArgs.Item);
}
}
}
else
{
if (!SelectedItems.Contains(eventArgs.Item))
{
SelectedItems.Clear();
SelectedItems.Add(eventArgs.Item);
await SelectedItemChanged.InvokeAsync(eventArgs.Item);
}
}
await SelectedItemChanged.InvokeAsync(SelectedItem);
await OnRowClick.InvokeAsync(eventArgs);
}
protected string RowStyleFuncInternal(T item, int index)
{
var style = "user-select: none; cursor: pointer;";
if (EqualityComparer<T>.Default.Equals(item, SelectedItem))
//EqualityComparer<T>.Default.Equals(item, SelectedItem) ||
if (SelectedItems.Contains(item))
{
style += " background-color: var(--mud-palette-grey-dark); color: var(--mud-palette-grey-light) !important;";
}

View File

@@ -7,7 +7,6 @@
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"

View File

@@ -42,7 +42,14 @@ else if (RenderType == RenderType.Children)
<MudList Clickable="true">
@foreach (var action in parent.Children)
{
<MudListItem Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
@if (action is Divider)
{
<MudDivider />
}
else
{
<MudListItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">@action.Name</MudListItem>
}
}
</MudList>
}

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Interop;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
@@ -107,39 +108,6 @@ namespace Lantean.QBTMudBlade.Components
await ApiClient.SetTorrentCategory(category, null, Hashes.ToArray());
}
protected async Task AddCategory()
{
await DialogService.InvokeAddCategoryDialog(ApiClient, Hashes);
}
protected async Task ResetCategory()
{
await ApiClient.SetTorrentCategory("", null, Hashes.ToArray());
}
protected async Task AddTag()
{
await DialogService.ShowSingleFieldDialog("Add Tags", "Comma-separated tags", "", v => ApiClient.AddTorrentTags(v.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries), null, Hashes.ToArray()));
}
protected async Task RemoveTags()
{
var torrents = GetTorrents();
foreach (var torrent in torrents)
{
await ApiClient.RemoveTorrentTags(torrent.Tags, null, torrent.Hash);
}
}
protected async Task ToggleTag(string tag)
{
var torrents = GetTorrents();
await ApiClient.RemoveTorrentTag(tag, torrents.Where(t => t.Tags.Contains(tag)).Select(t => t.Hash));
await ApiClient.AddTorrentTag(tag, torrents.Where(t => !t.Tags.Contains(tag)).Select(t => t.Hash));
}
protected async Task ToggleAutoTMM()
{
var torrents = GetTorrents();
@@ -231,9 +199,29 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected async Task ShowTags()
{
var parameters = new DialogParameters
{
{ nameof(ManageTagsDialog.Hashes), Hashes }
};
await DialogService.ShowAsync<ManageTagsDialog>("Manage Torrent Tags", parameters, DialogHelper.FormDialogOptions);
}
protected async Task ShowCategories()
{
var parameters = new DialogParameters
{
{ nameof(ManageCategoriesDialog.Hashes), Hashes }
};
await DialogService.ShowAsync<ManageCategoriesDialog>("Manage Torrent Categories", parameters, DialogHelper.FormDialogOptions);
}
protected async Task SubMenuTouch(TorrentAction action)
{
await DialogService.ShowSubMenu(Hashes, action);
await DialogService.ShowSubMenu(Hashes, action, MainData, Preferences);
}
private IEnumerable<Torrent> GetTorrents()
@@ -249,12 +237,16 @@ namespace Lantean.QBTMudBlade.Components
private List<TorrentAction>? _actions;
private IReadOnlyList<TorrentAction> Actions
private IEnumerable<TorrentAction> Actions
{
get
{
if (_actions is not null)
{
if (Preferences?.QueueingEnabled == false)
{
return _actions.Where(a => a.Name != "Queue");
}
return _actions;
}
@@ -268,22 +260,6 @@ namespace Lantean.QBTMudBlade.Components
}
}
var categories = new List<TorrentAction>
{
new TorrentAction("New", Icons.Material.Filled.Add, Color.Info, CreateCallback(AddCategory)),
new TorrentAction("Reset", Icons.Material.Filled.Remove, Color.Error, CreateCallback(ResetCategory)),
new Divider()
};
categories.AddRange(MainData.Categories.Select(c => new TorrentAction(c.Value.Name, Icons.Material.Filled.List, Color.Info, CreateCallback(() => SetCategory(c.Key)))));
var tags = new List<TorrentAction>
{
new TorrentAction("Add", Icons.Material.Filled.Add, Color.Info, CreateCallback(AddTag)),
new TorrentAction("Remove All", Icons.Material.Filled.Remove, Color.Error, CreateCallback(RemoveTags)),
new Divider()
};
tags.AddRange(MainData.Tags.Select(t => new TorrentAction(t, (torrent?.Tags.Contains(t) == true) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, CreateCallback(() => ToggleTag(t)))));
_actions = new List<TorrentAction>
{
new TorrentAction("Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
@@ -293,8 +269,8 @@ namespace Lantean.QBTMudBlade.Components
new Divider(),
new TorrentAction("Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation)),
new TorrentAction("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)),
new TorrentAction("Category", Icons.Material.Filled.List, Color.Info, categories, true),
new TorrentAction("Tags", Icons.Material.Filled.Label, Color.Info, tags, true),
new TorrentAction("Category", Icons.Material.Filled.List, Color.Info, CreateCallback(ShowCategories)),
new TorrentAction("Tags", Icons.Material.Filled.Label, Color.Info, CreateCallback(ShowTags)),
new TorrentAction("Automatic Torrent Management", Icons.Material.Filled.Check, (torrent?.AutomaticTorrentManagement == true) ? Color.Info : Color.Transparent, CreateCallback(ToggleAutoTMM)),
new Divider(),
new TorrentAction("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, CreateCallback(LimitUploadRate)),
@@ -322,18 +298,13 @@ namespace Lantean.QBTMudBlade.Components
new TorrentAction("Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
};
if (Preferences?.QueueingEnabled == false)
{
_actions.RemoveAt(18);
}
return _actions;
}
}
private EventCallback CreateCallback(Func<Task> action)
private EventCallback CreateCallback(Func<Task> action, bool ignoreAfterAction = false)
{
if (AfterAction is not null)
if (AfterAction is not null && !ignoreAfterAction)
{
return EventCallback.Factory.Create(this, async () =>
{
@@ -392,7 +363,7 @@ namespace Lantean.QBTMudBlade.Components
Children = [];
}
public TorrentAction(string name, string? icon, Color color, IEnumerable<TorrentAction> children, bool useTextButton = false)
public TorrentAction(string name, string? icon, Color color, IEnumerable<TorrentAction> children, bool multiAction = false, bool useTextButton = false)
{
Name = name;
Icon = icon;
@@ -413,5 +384,7 @@ namespace Lantean.QBTMudBlade.Components
public IEnumerable<TorrentAction> Children { get; }
public bool UseTextButton { get; }
public bool MultiAction { get; }
}
}

View File

@@ -7,7 +7,6 @@
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"

View File

@@ -7,7 +7,6 @@
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"

View File

@@ -4,6 +4,8 @@
{
public const string Magnet = @"<path fill=""currentColor"" d=""M3 7v6a9 9 0 0 0 9 9a9 9 0 0 0 9-9V7h-4v6a5 5 0 0 1-5 5a5 5 0 0 1-5-5V7m10-2h4V2h-4M3 5h4V2H3""/>";
public const string Random = @"<path d=""M21.67 3.955l-2.825-2.202.665-.753 4.478 3.497-4.474 3.503-.665-.753 2.942-2.292h-4.162c-3.547.043-5.202 3.405-6.913 7.023 1.711 3.617 3.366 6.979 6.913 7.022h4.099l-2.883-2.247.665-.753 4.478 3.497-4.474 3.503-.665-.753 2.884-2.247h-4.11c-3.896-.048-5.784-3.369-7.461-6.858-1.687 3.51-3.592 6.842-7.539 6.858h-2.623v-1h2.621c3.6-.014 5.268-3.387 6.988-7.022-1.72-3.636-3.388-7.009-6.988-7.023h-2.621v-1h2.623c3.947.016 5.852 3.348 7.539 6.858 1.677-3.489 3.565-6.81 7.461-6.858h4.047z""/>";
public const string Random = @"<path fill=""currentColor"" d=""M21.67 3.955l-2.825-2.202.665-.753 4.478 3.497-4.474 3.503-.665-.753 2.942-2.292h-4.162c-3.547.043-5.202 3.405-6.913 7.023 1.711 3.617 3.366 6.979 6.913 7.022h4.099l-2.883-2.247.665-.753 4.478 3.497-4.474 3.503-.665-.753 2.884-2.247h-4.11c-3.896-.048-5.784-3.369-7.461-6.858-1.687 3.51-3.592 6.842-7.539 6.858h-2.623v-1h2.621c3.6-.014 5.268-3.387 6.988-7.022-1.72-3.636-3.388-7.009-6.988-7.023h-2.621v-1h2.623c3.947.016 5.852 3.348 7.539 6.858 1.677-3.489 3.565-6.81 7.461-6.858h4.047z""/>";
public const string RadioIndeterminate = @"<path fill=""currentColor"" d=""M0 0h24v24H0z""/><path d=""M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm-5-9h10v2H7v-2z""/>";
}
}

View File

@@ -4,14 +4,19 @@ using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Filter;
using Lantean.QBTMudBlade.Models;
using MudBlazor;
using System.Collections.Generic;
namespace Lantean.QBTMudBlade
{
public static class DialogHelper
{
public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, ClassBackground = "background-blur" };
public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, ClassBackground = "background-blur", FullWidth = true };
private static readonly DialogOptions _confirmDialogOptions = new() { ClassBackground = "background-blur" };
public static readonly DialogOptions NonBlurFormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
public static readonly DialogOptions ConfirmDialogOptions = new() { ClassBackground = "background-blur", MaxWidth = MaxWidth.Small, FullWidth = true };
public static readonly DialogOptions NonBlurConfirmDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
public const long _maxFileSize = 4194304;
@@ -109,23 +114,35 @@ namespace Lantean.QBTMudBlade
await Task.Delay(0);
}
public static async Task InvokeAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<string>? hashes = null)
public static async Task<string?> ShowAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient)
{
var reference = await dialogService.ShowAsync<DeleteDialog>("New Category");
var reference = await dialogService.ShowAsync<AddCategoryDialog>("New Category", NonBlurFormDialogOptions);
var result = await reference.Result;
if (result.Canceled)
{
return;
return null;
}
var category = (Category)result.Data;
await apiClient.AddCategory(category.Name, category.SavePath);
if (hashes is not null)
return category.Name;
}
public static async Task<HashSet<string>?> ShowAddTagsDialog(this IDialogService dialogService, IApiClient apiClient)
{
var dialogReference = await dialogService.ShowAsync<AddTagDialog>("Add Tags", NonBlurFormDialogOptions);
var result = await dialogReference.Result;
if (result.Canceled)
{
await apiClient.SetTorrentCategory(category.Name, null, hashes.ToArray());
return null;
}
var tags = (HashSet<string>)result.Data;
return tags;
}
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func<Task> onSuccess)
@@ -134,7 +151,7 @@ namespace Lantean.QBTMudBlade
{
{ nameof(ConfirmDialog.Content), content }
};
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, _confirmDialogOptions);
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
@@ -162,7 +179,7 @@ namespace Lantean.QBTMudBlade
{ nameof(SingleFieldDialog<T>.Label), label },
{ nameof(SingleFieldDialog<T>.Value), value }
};
var result = await dialogService.ShowAsync<SingleFieldDialog<T>>(title, parameters, _confirmDialogOptions);
var result = await dialogService.ShowAsync<SingleFieldDialog<T>>(title, parameters, FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
@@ -251,15 +268,17 @@ namespace Lantean.QBTMudBlade
await Task.Delay(0);
}
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, TorrentAction parent)
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, TorrentAction parent, MainData mainData, QBitTorrentClient.Models.Preferences? preferences)
{
var parameters = new DialogParameters
{
{ nameof(SubMenuDialog.ParentAction), parent },
{ nameof(SubMenuDialog.Hashes), hashes }
{ nameof(SubMenuDialog.Hashes), hashes },
{ nameof(SubMenuDialog.MainData), mainData },
{ nameof(SubMenuDialog.Preferences), preferences },
};
await dialogService.ShowAsync<SubMenuDialog>("Actions", parameters, FormDialogOptions);
await dialogService.ShowAsync<SubMenuDialog>(parent.Name, parameters, FormDialogOptions);
}
}
}
}

View File

@@ -22,4 +22,10 @@
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
</ItemGroup>
<ItemGroup>
<Content Update="Components\Dialogs\ManageCategoriesDialog.razor">
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
</Content>
</ItemGroup>
</Project>

View File

@@ -2,39 +2,40 @@
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@if (ErrorBoundary?.Errors.Count > 0)
{
<MudBadge Content="@(ErrorBoundary?.Errors.Count ?? 0)" Color="Color.Error" Overlap="true">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge>
}
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" @bind-Value="IsDarkMode" />
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="Theme">
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</MudLayout>
</EnhancedErrorBoundary>
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@if (ErrorBoundary?.Errors.Count > 0)
{
<MudBadge Content="@(ErrorBoundary?.Errors.Count ?? 0)" Color="Color.Error" Overlap="true">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge>
}
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" @bind-Value="IsDarkMode" />
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="Theme">
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</MudLayout>

View File

@@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG
protected override async Task OnInitializedAsync()
{
await DoLogin("admin", "u4FR4ZQCm");
await DoLogin("admin", "U9zfhvndk");
}
#endif
}

View File

@@ -23,7 +23,6 @@
OnRowClick="RowClick"
MultiSelection="true"
SelectedItemsChanged="SelectedItemsChanged"
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChangedHandler"
SortDirectionChanged="SortDirectionChangedHandler"
/>

View File

@@ -39,21 +39,15 @@ namespace Lantean.QBTMudBlade.Pages
protected string? SearchText { get; set; }
protected Torrent? SelectedTorrent { get; set; }
protected HashSet<Torrent> SelectedItems { get; set; } = [];
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0 || SelectedTorrent is not null;
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
protected DynamicTable<Torrent>? Table { get; set; }
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
{
SelectedItems = selectedItems;
if (selectedItems.Count == 1)
{
SelectedTorrent = selectedItems.First();
}
}
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
@@ -61,11 +55,6 @@ namespace Lantean.QBTMudBlade.Pages
await SortDirectionChanged.InvokeAsync(sortDirection);
}
protected void SelectedItemChanged(Torrent torrent)
{
SelectedTorrent = torrent;
}
protected async Task SortColumnChangedHandler(string columnId)
{
await SortColumnChanged.InvokeAsync(columnId);
@@ -122,7 +111,12 @@ namespace Lantean.QBTMudBlade.Pages
{
if (eventArgs.MouseEventArgs.Detail > 1)
{
NavigationManager.NavigateTo("/details/" + SelectedTorrent);
var torrent = SelectedItems.FirstOrDefault();
if (torrent is null)
{
return;
}
NavigationManager.NavigateTo($"/details/{torrent.Hash}");
}
}
@@ -133,11 +127,6 @@ namespace Lantean.QBTMudBlade.Pages
return SelectedItems.Select(t => t.Hash);
}
if (SelectedTorrent is not null)
{
return [SelectedTorrent.Hash];
}
return [];
}
@@ -158,11 +147,12 @@ namespace Lantean.QBTMudBlade.Pages
protected void ShowTorrent()
{
if (SelectedTorrent is null)
var torrent = SelectedItems.FirstOrDefault();
if (torrent is null)
{
return;
}
NavigationManager.NavigateTo("/details/" + SelectedTorrent.Hash);
NavigationManager.NavigateTo($"/details/{torrent.Hash}");
}
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions;
@@ -170,7 +160,7 @@ namespace Lantean.QBTMudBlade.Pages
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
[
CreateColumnDefinition("#", t => t.Priority),
CreateColumnDefinition("", t => t.State, IconColumn, iconOnly: true),
CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true),
CreateColumnDefinition("Name", t => t.Name, width: 400),
CreateColumnDefinition("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
CreateColumnDefinition("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),

View File

@@ -133,7 +133,7 @@ td.no-wrap {
}
.sub-menu::after {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E");
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z' fill='rgba(39,44,52,1)'/%3E%3C/svg%3E");
width: 25px;
height: 25px;
position: absolute;

View File

@@ -798,7 +798,7 @@ namespace Lantean.QBitTorrentClient
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.AddPipeSeparated("tags", tags)
.AddCommaSeparated("tags", tags)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/addTags", content);
@@ -810,7 +810,7 @@ namespace Lantean.QBitTorrentClient
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.AddPipeSeparated("tags", tags)
.AddCommaSeparated("tags", tags)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/removeTags", content);
@@ -830,7 +830,7 @@ namespace Lantean.QBitTorrentClient
public async Task CreateTags(IEnumerable<string> tags)
{
var content = new FormUrlEncodedBuilder()
.AddPipeSeparated("tags", tags)
.AddCommaSeparated("tags", tags)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/createTags", content);
@@ -841,7 +841,7 @@ namespace Lantean.QBitTorrentClient
public async Task DeleteTags(IEnumerable<string> tags)
{
var content = new FormUrlEncodedBuilder()
.AddPipeSeparated("tags", tags)
.AddCommaSeparated("tags", tags)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/deleteTags", content);

View File

@@ -66,11 +66,31 @@ namespace Lantean.QBitTorrentClient
return apiClient.SetTorrentCategory(category, null, hash);
}
public static Task SetTorrentCategory(this IApiClient apiClient, string category, IEnumerable<string> hashes)
{
return apiClient.SetTorrentCategory(category, null, hashes.ToArray());
}
public static Task RemoveTorrentCategory(this IApiClient apiClient, string hash)
{
return apiClient.SetTorrentCategory(string.Empty, null, hash);
}
public static Task RemoveTorrentCategory(this IApiClient apiClient, IEnumerable<string> hashes)
{
return apiClient.SetTorrentCategory(string.Empty, null, hashes.ToArray());
}
public static Task RemoveTorrentTags(this IApiClient apiClient, IEnumerable<string> tags, string hash)
{
return apiClient.RemoveTorrentTags(tags, null, hash);
}
public static Task RemoveTorrentTags(this IApiClient apiClient, IEnumerable<string> tags, IEnumerable<string> hashes)
{
return apiClient.RemoveTorrentTags(tags, null, hashes.ToArray());
}
public static Task RemoveTorrentTag(this IApiClient apiClient, string tag, string hash)
{
return apiClient.RemoveTorrentTags([tag], hash);
@@ -86,6 +106,11 @@ namespace Lantean.QBitTorrentClient
return apiClient.AddTorrentTags(tags, null, hash);
}
public static Task AddTorrentTags(this IApiClient apiClient, IEnumerable<string> tags, IEnumerable<string> hashes)
{
return apiClient.AddTorrentTags(tags, null, hashes.ToArray());
}
public static Task AddTorrentTag(this IApiClient apiClient, string tag, string hash)
{
return apiClient.AddTorrentTags([tag], hash);
@@ -95,6 +120,7 @@ namespace Lantean.QBitTorrentClient
{
return apiClient.AddTorrentTags([tag], null, hashes.ToArray());
}
public static Task RecheckTorrent(this IApiClient apiClient, string hash)
{

View File

@@ -14,14 +14,13 @@ namespace Lantean.QBitTorrentClient.Converters
List<string> list;
var value = reader.GetString();
if (value is null)
if (string.IsNullOrEmpty(value))
{
list = [];
}
else
{
var values = value.Split(',');
list = [.. values];
list = [.. value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)];
}
return list.AsReadOnly();

View File

@@ -22,6 +22,7 @@ namespace Lantean.QBitTorrentClient.Models
long? estimatedTimeOfArrival,
bool? firstLastPiecePriority,
bool? forceStart,
string hash,
string? infoHashV1,
string? infoHashV2,
long? lastActivity,
@@ -70,6 +71,7 @@ namespace Lantean.QBitTorrentClient.Models
EstimatedTimeOfArrival = estimatedTimeOfArrival;
FirstLastPiecePriority = firstLastPiecePriority;
ForceStart = forceStart;
Hash = hash;
InfoHashV1 = infoHashV1;
InfoHashV2 = infoHashV2;
LastActivity = lastActivity;
@@ -149,6 +151,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("force_start")]
public bool? ForceStart { get; }
[JsonPropertyName("hash")]
public string Hash { get; }
[JsonPropertyName("infohash_v1")]
public string? InfoHashV1 { get; }