Add dynamic table and fix several bugs

This commit is contained in:
ahjephson
2024-05-17 20:29:05 +01:00
parent dc815124f7
commit 79847a6ad8
24 changed files with 819 additions and 416 deletions

View File

@@ -3,14 +3,25 @@
<MudDialog>
<DialogContent>
<MudCard Class="w-100">
<MudList>
@foreach (var column in Columns)
<MudGrid>
@for (var i = 0; i < Columns.Count; i++)
{
<MudListItem>
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumns.Contains(column.Id))" />
</MudListItem>
var column = Columns[i];
var index = i;
<MudItem xs="7">
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumns.Contains(column.Id))" />
</MudItem>
<MudItem xs="3">
<MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" ShrinkLabel="true" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
</MudItem>
<MudItem xs="1">
<MudIconButton Icon="@Icons.Material.Outlined.KeyboardArrowUp" Disabled="@(index == 0)" OnClick="@(e => MoveUp(index))" />
</MudItem>
<MudItem xs="1">
<MudIconButton Icon="@Icons.Material.Outlined.KeyboardArrowDown" Disabled="@(index == Columns.Count - 1)" OnClick="@(e => MoveDown(index))" />
</MudItem>
}
</MudList>
</MudGrid>
</MudCard>
</DialogContent>
<DialogActions>

View File

@@ -13,6 +13,9 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[EditorRequired]
public List<ColumnDefinition<T>> Columns { get; set; } = default!;
[Parameter]
public Dictionary<string, int?> Widths { get; set; } = [];
protected HashSet<string> SelectedColumns { get; set; } = [];
protected override void OnParametersSet()
@@ -26,7 +29,7 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
}
}
protected void SetSelected(bool selected, string id)
protected void SetSelected(bool selected, string id)
{
if (selected)
{
@@ -38,6 +41,75 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
}
}
protected void SetWidth(string? value, string id)
{
var column = Columns.Find(c => c.Id == id);
var defaultWidth = column?.Width;
if (int.TryParse(value, out var width))
{
if (width == defaultWidth)
{
Widths.Remove(id);
}
else
{
Widths[id] = width;
}
}
else
{
if (defaultWidth is null)
{
Widths.Remove(id);
}
else
{
Widths[id] = null;
}
}
}
protected void MoveUp(int index)
{
if (index == 0)
{
return;
}
(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
}
protected void MoveDown(int index)
{
if (index >= Columns.Count)
{
return;
}
(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
}
protected string GetValue(int? value, string columnId)
{
if (Widths.TryGetValue(columnId, out var newWidth))
{
value = newWidth;
}
if (!value.HasValue)
{
return "";
}
if (value.Value <= 0)
{
return "auto";
}
return value.Value.ToString();
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
@@ -45,7 +117,7 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(SelectedColumns));
MudDialog.Close(DialogResult.Ok((SelectedColumns, Widths)));
}
}
}

View File

@@ -0,0 +1,58 @@
@typeparam T
@inherits MudComponentBase
<MudTable
@ref="Table"
Items="OrderedItems"
T="T"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Loading="@(Items is null)"
SelectOnRowClick="false"
Striped="Striped"
Square="true"
LoadingProgressColor="Color.Info"
MultiSelection="MultiSelection"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChangedInternal"
HorizontalScrollbar="true"
OnRowClick="OnRowClickInternal"
RowStyleFunc="RowStyleFuncInternal"
Virtualize="true"
AllowUnsorted="false"
Class="@Class">
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())
{
<col style="@(GetColumnStyle(column))" />
}
</ColGroup>
<HeaderContent>
@foreach (var column in GetColumns())
{
<MudTh Class="overflow-cell" Style="@(GetColumnStyle(column))">
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="T" SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@column.Header</MudTableSortLabel>
}
else
{
@column.Header
}
</MudTh>
}
</HeaderContent>
<RowTemplate>
@foreach (var column in GetColumns())
{
<MudTd DataLabel="@column.Header" Class="@(GetColumnClass(column, context))" Style="@(GetColumnStyle(column))">
@column.RowTemplate(column.GetRowContext(context))
</MudTd>
}
</RowTemplate>
</MudTable>

View File

@@ -0,0 +1,279 @@
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Data.Common;
namespace Lantean.QBTMudBlade.Components
{
public partial class DynamicTable<T> : MudComponentBase
{
private static readonly string _typeName = typeof(T).Name;
private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
[Inject]
public ILocalStorageService LocalStorage { get; set; } = default!;
[Inject]
public IDialogService DialogService { get; set; } = default!;
[Parameter]
[EditorRequired]
public IEnumerable<ColumnDefinition<T>> ColumnDefinitions { get; set; } = [];
[Parameter]
[EditorRequired]
public IEnumerable<T>? Items { get; set; }
[Parameter]
public bool MultiSelection { get; set; }
[Parameter]
public bool Striped { get; set; }
[Parameter]
public bool Hover { get; set; }
[Parameter]
public bool PreSorted { get; set; }
[Parameter]
public bool SelectOnRowClick { get; set; }
[Parameter]
public EventCallback<TableRowClickEventArgs<T>> OnRowClick { get; set; }
[Parameter]
public HashSet<T> SelectedItems { get; set; } = [];
[Parameter]
public EventCallback<HashSet<T>> SelectedItemsChanged { get; set; }
[Parameter]
public EventCallback<T> SelectedItemChanged { get; set; }
[Parameter]
public T? SelectedItem { get; set; }
[Parameter]
public Func<ColumnDefinition<T>, bool> ColumnFilter { get; set; } = (t => true);
[Parameter]
public EventCallback<string> SortColumnChanged { get; set; }
[Parameter]
public EventCallback<SortDirection> SortDirectionChanged { get; set; }
[Parameter]
public EventCallback<HashSet<string>> SelectedColumnsChanged { get; set; }
protected IEnumerable<T>? OrderedItems => GetOrderedItems();
protected HashSet<string> SelectedColumns { get; set; } = [];
private Dictionary<string, int?> _columnWidths = [];
private MudTable<T>? Table { get; set; }
private string? _sortColumn;
private SortDirection _sortDirection;
protected override async Task OnInitializedAsync()
{
HashSet<string> selectedColumns;
var storedSelectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnSelectionStorageKey);
if (storedSelectedColumns is not null)
{
selectedColumns = storedSelectedColumns;
}
else
{
selectedColumns = ColumnDefinitions.Where(c => c.Enabled).Select(c => c.Id).ToHashSet();
}
if (!SelectedColumns.SetEquals(selectedColumns))
{
SelectedColumns = selectedColumns;
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
}
string? sortColumn;
SortDirection sortDirection;
var storedColumnSort = await LocalStorage.GetItemAsync<Tuple<string, SortDirection>>(_columnSortStorageKey);
if (storedColumnSort is not null)
{
sortColumn = storedColumnSort.Item1;
sortDirection = storedColumnSort.Item2;
}
else
{
sortColumn = ColumnDefinitions.First(c => c.Enabled).Id;
sortDirection = SortDirection.Ascending;
}
if (_sortColumn != sortColumn)
{
_sortColumn = sortColumn;
await SortColumnChanged.InvokeAsync(_sortColumn);
}
if (_sortDirection != sortDirection)
{
_sortDirection = sortDirection;
await SortDirectionChanged.InvokeAsync(_sortDirection);
}
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
if (storedColumnsWidths is not null)
{
_columnWidths = storedColumnsWidths;
}
}
private IEnumerable<T>? GetOrderedItems()
{
if (Items is null)
{
return null;
}
if (PreSorted)
{
return Items;
}
var sortSelector = ColumnDefinitions.FirstOrDefault(c => c.Id == _sortColumn)?.SortSelector;
if (sortSelector is null)
{
return Items;
}
return Items.OrderByDirection(_sortDirection, sortSelector);
}
protected IEnumerable<ColumnDefinition<T>> GetColumns()
{
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
foreach (var column in filteredColumns)
{
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
}
yield return column;
}
}
private async Task SetSort(string columnId, SortDirection sortDirection)
{
if (_sortColumn != columnId)
{
_sortColumn = columnId;
await SortColumnChanged.InvokeAsync(_sortColumn);
}
if (_sortDirection != sortDirection)
{
_sortDirection = sortDirection;
await SortDirectionChanged.InvokeAsync(_sortDirection);
}
await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple<string, SortDirection>(columnId, sortDirection));
}
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
{
SelectedItem = 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))
{
style += " background-color: var(--mud-palette-dark-darken)";
}
return style;
}
protected async Task SelectedItemsChangedInternal(HashSet<T> selectedItems)
{
await SelectedItemsChanged.InvokeAsync(selectedItems);
}
public async Task ShowColumnOptionsDialog()
{
var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), _columnWidths);
if (result == default)
{
return;
}
if (!SelectedColumns.SetEquals(result.SelectedColumns))
{
SelectedColumns = result.SelectedColumns;
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
}
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
{
_columnWidths = result.ColumnWidths;
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
}
}
private static bool DictionaryEqual(Dictionary<string, int?> left, Dictionary<string, int?> right)
{
return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && left[k] == right[k]);
}
private static string? GetColumnStyle(ColumnDefinition<T> column)
{
string? style = null;
if (column.Width.HasValue)
{
style = $"width: {column.Width.Value}px; max-width: {column.Width.Value}px;";
}
return style;
}
private static string? GetColumnClass(ColumnDefinition<T> column, T data)
{
var className = column.Class;
if (column.ClassFunc is not null)
{
var funcClass = column.ClassFunc(data);
if (funcClass is not null)
{
if (className is null)
{
className = funcClass;
}
else
{
className = $"{className} {column.ClassFunc(data)}";
}
}
}
if (column.Width.HasValue)
{
className = $"overflow-cell {className}";
}
return className;
}
}
}

View File

@@ -18,48 +18,20 @@
<MudSpacer />
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
<MudTable T="ContentItem" Hover="true" FixedHeader="true" HeaderClass="table-head-bordered" Breakpoint="Breakpoint.None" Bordered="false"
MultiSelection="true" Dense="true" SelectOnRowClick="false"
Items="Files"
SelectedItems="SelectedItems"
<DynamicTable
@ref="Table"
T="ContentItem"
ColumnDefinitions="Columns"
Items="Files"
MultiSelection="true"
PreSorted="true"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChanged"
OnRowClick="RowClick"
RowStyleFunc="RowStyle"
RowClassFunc="RowClass"
AllowUnsorted="false"
Virtualize="true">
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())
{
var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null;
<col style="@style" />
}
</ColGroup>
<HeaderContent>
@foreach (var column in GetColumns())
{
<MudTh>
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="ContentItem" SortDirectionChanged="@(c => SetSort(column.Id, c))" InitialDirection="column.InitialDirection">@column.Header</MudTableSortLabel>
}
else
{
@column.Header
}
</MudTh>
}
</HeaderContent>
<RowTemplate>
@foreach (var column in GetColumns())
{
<MudTd DataLabel="@column.Header" Class="@column.Class">
@column.RowTemplate(column.GetRowContext(context))
</MudTd>
}
</RowTemplate>
</MudTable>
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChanged"
SortDirectionChanged="SortDirectionChanged"
/>
@code {
private RenderFragment<RowContext<ContentItem>> NameColumn
@@ -68,11 +40,11 @@
{
return context => __builder =>
{
<div style="@($"margin-left: {context.Data.Level * 56}px")">
<div style="@($"margin-left: {context.Data.Level * 14}px")">
@if (context.Data.IsFolder)
{
<MudIconButton ButtonType="ButtonType.Button" Icon="@Icons.Material.Filled.ExpandLess" Class="@("pa-0 " + (ExpandedNodes.Contains(context.Data.Name) ? "rotate-180" : "rotate-90"))" OnClick="@(c => ToggleNode(context.Data, c))"></MudIconButton>
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-2" Style="margin-right: 4px; position: relative; top: 3px" />
<MudIconButton Edge="Edge.Start" ButtonType="ButtonType.Button" Icon="@(ExpandedNodes.Contains(context.Data.Name) ? Icons.Material.Filled.KeyboardArrowDown : Icons.Material.Filled.KeyboardArrowRight)" OnClick="@(c => ToggleNode(context.Data, c))"></MudIconButton>
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-0" Style="margin-right: 4px; position: relative; top: 7px; margin-left: -15px" />
}
@context.Data.DisplayName
</div>;
@@ -95,4 +67,19 @@
};
}
}
private static RenderFragment<RowContext<ContentItem>> ProgressBarColumn
{
get
{
return context => __builder =>
{
var value = (float?)context.GetValue();
var color = value < 1 ? Color.Success : Color.Info;
<MudProgressLinear title="Progress" Color="@color" Value="@((value ?? 0) * 100)" Class="progress-expand" Size="Size.Large">
@DisplayHelpers.Percentage(value)
</MudProgressLinear>;
};
}
}
}

View File

@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Collections.ObjectModel;
using System.Net;
using System.Linq;
namespace Lantean.QBTMudBlade.Components
{
@@ -16,16 +17,10 @@ namespace Lantean.QBTMudBlade.Components
{
private readonly bool _refreshEnabled = true;
private const string _columnSelectionStorageKey = "FilesTab.ColumnSelection";
private const string _columnSortStorageKey = "FilesTab.ColumnSort";
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
private string? _sortColumn;
private SortDirection _sortDirection = SortDirection.Ascending;
[Parameter]
public bool Active { get; set; }
@@ -44,9 +39,6 @@ namespace Lantean.QBTMudBlade.Components
[Inject]
protected IDataManager DataManager { get; set; } = default!;
[Inject]
protected ILocalStorageService LocalStorage { get; set; } = default!;
protected HashSet<string> ExpandedNodes { get; set; } = [];
protected Dictionary<string, ContentItem>? FileList { get; set; }
@@ -55,70 +47,35 @@ namespace Lantean.QBTMudBlade.Components
protected HashSet<ContentItem> SelectedItems { get; set; } = [];
protected List<ColumnDefinition<ContentItem>> _columns = [];
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
protected ContentItem? SelectedItem { get; set; }
protected string? SearchText { get; set; }
protected int? _selectedIndex { get; set; }
protected HashSet<string> SelectedColumns { get; set; }
public IEnumerable<Func<ContentItem, bool>>? Filters { get; set; }
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
private DynamicTable<ContentItem>? Table { get; set; }
private string? _sortColumn;
private SortDirection _sortDirection;
public FilesTab()
{
_columns.Add(CreateColumnDefinition("Name", c => c.Name, NameColumn, width: 200, initialDirection: SortDirection.Ascending));
_columns.Add(CreateColumnDefinition("Total Size", c => c.Size, c => DisplayHelpers.Size(c.Size)));
_columns.Add(CreateColumnDefinition("Progress", c => c.Progress, c => DisplayHelpers.Percentage(c.Progress)));
_columns.Add(CreateColumnDefinition("Priority", c => c.Priority, PriorityColumn));
_columns.Add(CreateColumnDefinition("Remaining", c => c.Remaining, c => DisplayHelpers.Size(c.Remaining)));
_columns.Add(CreateColumnDefinition("Availability", c => c.Availability, c => c.Availability.ToString("0.00")));
SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet();
}
protected override async Task OnInitializedAsync()
{
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnSelectionStorageKey);
if (selectedColumns is not null)
{
SelectedColumns = selectedColumns;
}
var columnSort = await LocalStorage.GetItemAsync<Tuple<string, SortDirection>>(_columnSortStorageKey);
if (columnSort is not null)
{
_sortColumn = columnSort.Item1;
_sortDirection = columnSort.Item2;
}
}
protected IEnumerable<ColumnDefinition<ContentItem>> GetColumns()
{
return _columns.Where(c => SelectedColumns.Contains(c.Id));
_columnRenderFragments.Add("Name", NameColumn);
_columnRenderFragments.Add("Priority", PriorityColumn);
}
protected async Task ColumnOptions()
{
DialogParameters parameters = new DialogParameters
{
{ "Columns", _columns }
};
var reference = await DialogService.ShowAsync<ColumnOptionsDialog<ContentItem>>("ColumnOptions", parameters, DialogHelper.FormDialogOptions);
var result = await reference.Result;
if (result.Canceled)
if (Table is null)
{
return;
}
SelectedColumns = (HashSet<string>)result.Data;
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
await Table.ShowColumnOptionsDialog();
}
protected async Task ShowFilterDialog()
@@ -306,26 +263,13 @@ namespace Lantean.QBTMudBlade.Components
await ApiClient.SetFilePriority(Hash, fileIndexes, MapPriority(priority));
}
protected string RowClass(ContentItem contentItem, int index)
{
if (contentItem.Level == 0)
{
return "d-table-row";
}
if (ExpandedNodes.Contains(contentItem.Path))
{
return "d-table-row";
}
return "d-none";
}
protected async Task RenameFile()
{
if (Hash is null || FileList is null || _selectedIndex is null)
if (Hash is null || FileList is null || SelectedItem is null)
{
return;
}
var contentItem = FileList.Values.FirstOrDefault(c => c.Index == _selectedIndex.Value);
var contentItem = FileList.Values.FirstOrDefault(c => c.Index == SelectedItem.Index);
if (contentItem is null)
{
return;
@@ -334,19 +278,19 @@ namespace Lantean.QBTMudBlade.Components
await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async v => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + v));
}
protected void RowClick(TableRowClickEventArgs<ContentItem> eventArgs)
protected void SortColumnChanged(string sortColumn)
{
_selectedIndex = eventArgs.Item.Index;
_sortColumn = sortColumn;
}
protected string RowStyle(ContentItem item, int index)
protected void SortDirectionChanged(SortDirection sortDirection)
{
var style = "user-select: none; cursor: pointer;";
if (_selectedIndex != item.Index)
{
return style;
}
return $"{style} background: #D3D3D3";
_sortDirection = sortDirection;
}
protected void SelectedItemChanged(ContentItem item)
{
SelectedItem = item;
}
protected async Task SelectedItemsChanged(HashSet<ContentItem> selectedItems)
@@ -386,14 +330,6 @@ namespace Lantean.QBTMudBlade.Components
}
}
private async Task SetSort(string columnId, SortDirection sortDirection)
{
_sortColumn = columnId;
_sortDirection = sortDirection;
await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple<string, SortDirection>(columnId, sortDirection));
}
protected void ToggleNode(ContentItem contentItem, MouseEventArgs args)
{
if (ExpandedNodes.Contains(contentItem.Name))
@@ -404,6 +340,11 @@ namespace Lantean.QBTMudBlade.Components
{
ExpandedNodes.Add(contentItem.Name);
}
if (FileList is not null)
{
SelectedItems = GetFiles().Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
}
private static QBitTorrentClient.Models.Priority MapPriority(Priority priority)
@@ -411,6 +352,13 @@ namespace Lantean.QBTMudBlade.Components
return (QBitTorrentClient.Models.Priority)(int)priority;
}
private Func<ContentItem, object?> GetSortSelector()
{
var sortSelector = ColumnsDefinitions.Find(c => c.Id == _sortColumn)?.SortSelector;
return sortSelector ?? (i => i.Name);
}
private IEnumerable<ContentItem> GetChildren(ContentItem contentItem)
{
if (!contentItem.IsFolder || Files is null)
@@ -421,28 +369,28 @@ namespace Lantean.QBTMudBlade.Components
return Files.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
}
private Func<ContentItem, object?> GetSortSelector()
{
var sortSelector = _columns.Find(c => c.Id == _sortColumn)?.SortSelector;
return sortSelector ?? (i => i.Name);
}
private IEnumerable<ContentItem> GetDescendants(ContentItem folder, int level)
private IEnumerable<ContentItem> GetChildren(ContentItem folder, int level)
{
level++;
var descendantsKey = folder.GetDescendantsKey(level);
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey)).OrderByDirection(_sortDirection, GetSortSelector()))
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey) && f.Level == level).OrderByDirection(_sortDirection, GetSortSelector()))
{
if (item.IsFolder)
{
var descendants = GetDescendants(item, level);
// if the filter returns some resutls then show folder item
var descendants = GetChildren(item, level);
// if the filter returns some results then show folder item
if (descendants.Any())
{
yield return item;
}
// if the folder is not expanded - don't return children
if (!ExpandedNodes.Contains(item.Name))
{
continue;
}
// then show children
foreach (var descendant in descendants)
{
@@ -497,15 +445,19 @@ namespace Lantean.QBTMudBlade.Components
var list = new List<ContentItem>();
var folders = FileList.Values.Where(c => c.IsFolder && c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
foreach (var folder in folders)
var rootItems = FileList.Values.Where(c => c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
foreach (var item in rootItems)
{
list.Add(folder);
var level = 0;
var descendants = GetDescendants(folder, level);
foreach (var descendant in descendants)
list.Add(item);
if (item.IsFolder && ExpandedNodes.Contains(item.Name))
{
list.Add(descendant);
var level = 0;
var descendants = GetChildren(item, level);
foreach (var descendant in descendants)
{
list.Add(descendant);
}
}
}
@@ -576,7 +528,32 @@ namespace Lantean.QBTMudBlade.Components
await ApiClient.SetFilePriority(Hash, files, priority);
}
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, RenderFragment<RowContext<ContentItem>> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
protected IEnumerable<ColumnDefinition<ContentItem>> Columns => GetColumnDefinitions();
private IEnumerable<ColumnDefinition<ContentItem>> GetColumnDefinitions()
{
foreach (var columnDefinition in ColumnsDefinitions)
{
if (_columnRenderFragments.TryGetValue(columnDefinition.Header, out var fragment))
{
columnDefinition.RowTemplate = fragment;
}
yield return columnDefinition;
}
}
public static List<ColumnDefinition<ContentItem>> ColumnsDefinitions { get; } =
[
CreateColumnDefinition("Name", c => c.Name, width: 200, initialDirection: SortDirection.Ascending, classFunc: c => c.IsFolder ? "pa-0" : "pa-3"),
CreateColumnDefinition("Total Size", c => c.Size, c => DisplayHelpers.Size(c.Size)),
CreateColumnDefinition("Progress", c => c.Progress, ProgressBarColumn, tdClass: "table-progress pl-2 pr-2"),
CreateColumnDefinition("Priority", c => c.Priority, tdClass: "table-select pa-0"),
CreateColumnDefinition("Remaining", c => c.Remaining, c => DisplayHelpers.Size(c.Remaining)),
CreateColumnDefinition("Availability", c => c.Availability, c => c.Availability.ToString("0.00")),
];
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, RenderFragment<RowContext<ContentItem>> rowTemplate, int? width = null, string? tdClass = null, Func<ContentItem, string?>? classFunc = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
{
var cd = new ColumnDefinition<ContentItem>(name, selector, rowTemplate);
cd.Class = "no-wrap";
@@ -584,6 +561,7 @@ namespace Lantean.QBTMudBlade.Components
{
cd.Class += " " + tdClass;
}
cd.ClassFunc = classFunc;
cd.Width = width;
cd.Enabled = enabled;
cd.InitialDirection = initialDirection;
@@ -591,7 +569,7 @@ namespace Lantean.QBTMudBlade.Components
return cd;
}
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, Func<ContentItem, string>? formatter = null, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, Func<ContentItem, string>? formatter = null, int? width = null, string? tdClass = null, Func<ContentItem, string?>? classFunc = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
{
var cd = new ColumnDefinition<ContentItem>(name, selector, formatter);
cd.Class = "no-wrap";
@@ -599,6 +577,7 @@ namespace Lantean.QBTMudBlade.Components
{
cd.Class += " " + tdClass;
}
cd.ClassFunc = classFunc;
cd.Width = width;
cd.Enabled = enabled;
cd.InitialDirection = initialDirection;

View File

@@ -2,6 +2,7 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using static MudBlazor.Colors;
namespace Lantean.QBTMudBlade.Components
{
@@ -53,24 +54,33 @@ namespace Lantean.QBTMudBlade.Components
protected override async Task OnInitializedAsync()
{
if (await LocalStorage.ContainKeyAsync(_statusSelectionStorageKey))
var status = await LocalStorage.GetItemAsStringAsync(_statusSelectionStorageKey);
if (status is not null)
{
Status = await LocalStorage.GetItemAsStringAsync(_statusSelectionStorageKey) ?? Models.Status.All.ToString();
Status = status;
await StatusChanged.InvokeAsync(Enum.Parse<Status>(status));
}
if (await LocalStorage.ContainKeyAsync(_categorySelectionStorageKey))
var category = await LocalStorage.GetItemAsStringAsync(_categorySelectionStorageKey);
if (category is not null)
{
Category = await LocalStorage.GetItemAsStringAsync(_categorySelectionStorageKey) ?? FilterHelper.CATEGORY_ALL;
Category = category;
await CategoryChanged.InvokeAsync(category);
}
if (await LocalStorage.ContainKeyAsync(_tagSelectionStorageKey))
{
Tag = await LocalStorage.GetItemAsStringAsync(_tagSelectionStorageKey) ?? FilterHelper.TAG_ALL;
}
if (await LocalStorage.ContainKeyAsync(_trackerSelectionStorageKey))
var tag = await LocalStorage.GetItemAsStringAsync(_tagSelectionStorageKey);
if (tag is not null)
{
Tracker = await LocalStorage.GetItemAsStringAsync(_trackerSelectionStorageKey) ?? FilterHelper.TRACKER_ALL;
Tag = tag;
await TagChanged.InvokeAsync(tag);
}
var tracker = await LocalStorage.GetItemAsStringAsync(_trackerSelectionStorageKey);
if (tracker is not null)
{
Tracker = tracker;
await TrackerChanged.InvokeAsync(tracker);
}
}
@@ -78,28 +88,60 @@ namespace Lantean.QBTMudBlade.Components
{
Status = value;
await StatusChanged.InvokeAsync(Enum.Parse<Status>(value));
await LocalStorage.SetItemAsStringAsync(_statusSelectionStorageKey, value);
if (value != Models.Status.All.ToString())
{
await LocalStorage.SetItemAsStringAsync(_statusSelectionStorageKey, value);
}
else
{
await LocalStorage.RemoveItemAsync(_statusSelectionStorageKey);
}
}
protected async Task CategoryValueChanged(string value)
{
Category = value;
await CategoryChanged.InvokeAsync(value);
await LocalStorage.SetItemAsStringAsync(_categorySelectionStorageKey, value);
if (value != FilterHelper.CATEGORY_ALL)
{
await LocalStorage.SetItemAsStringAsync(_categorySelectionStorageKey, value);
}
else
{
await LocalStorage.RemoveItemAsync(_categorySelectionStorageKey);
}
}
protected async Task TagValueChanged(string value)
{
Tag = value;
await TagChanged.InvokeAsync(value);
await LocalStorage.SetItemAsStringAsync(_tagSelectionStorageKey, value);
if (value != FilterHelper.TAG_ALL)
{
await LocalStorage.SetItemAsStringAsync(_tagSelectionStorageKey, value);
}
else
{
await LocalStorage.RemoveItemAsync(_tagSelectionStorageKey);
}
}
protected async Task TrackerValueChanged(string value)
{
Tracker = value;
await TrackerChanged.InvokeAsync(value);
await LocalStorage.SetItemAsStringAsync(_trackerSelectionStorageKey, value);
if (value != FilterHelper.TRACKER_ALL)
{
await LocalStorage.SetItemAsStringAsync(_trackerSelectionStorageKey, value);
}
else
{
await LocalStorage.RemoveItemAsync(_trackerSelectionStorageKey);
}
}
protected static string GetHostName(string tracker)

View File

@@ -1,7 +1,7 @@
<MudNavMenu>
<MudNavLink Icon="@(Icons.Material.Outlined.NavigateBefore)" OnClick="NavigateBack">Back</MudNavLink>
<MudDivider />
@if (Torrents is null)
@if (OrderedTorrents is null)
{
@for (var i = 0; i < 10; i++)
{
@@ -10,7 +10,7 @@
}
else
{
foreach (var torrent in Torrents)
foreach (var torrent in OrderedTorrents)
{
<MudNavLink Href="@("/details/" + torrent.Hash)">@torrent.Name</MudNavLink>
}

View File

@@ -1,5 +1,7 @@
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Pages;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
@@ -14,6 +16,26 @@ namespace Lantean.QBTMudBlade.Components
[Parameter]
public string? SelectedTorrent { get; set; }
[Parameter]
public SortDirection SortDirection { get; set; }
[Parameter]
public string? SortColumn { get; set; }
protected IEnumerable<Torrent>? OrderedTorrents => GetOrderedTorrents();
private IEnumerable<Torrent>? GetOrderedTorrents()
{
if (Torrents is null)
{
return null;
}
var sortSelector = TorrentList.ColumnsDefinitions.Find(t => t.Id == SortColumn)?.SortSelector ?? (t => t.Name);
return Torrents.OrderByDirection(SortDirection, sortSelector);
}
protected void NavigateBack()
{
NavigationManager.NavigateTo("/");

View File

@@ -226,6 +226,23 @@ namespace Lantean.QBTMudBlade
return (List<PropertyFilterDefinition<T>>?)dialogResult.Data;
}
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, Dictionary<string, int?> widths)
{
var parameters = new DialogParameters
{
{ nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
};
var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
var result = await reference.Result;
if (result.Canceled)
{
return default;
}
return ((HashSet<string>, Dictionary<string, int?>))result.Data;
}
public static async Task InvokeRssRulesDialog(this IDialogService dialogService)
{
await Task.Delay(0);

View File

@@ -2,7 +2,7 @@
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true">
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" />
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
</MudDrawer>
<MudMainContent>
@Body

View File

@@ -1,5 +1,6 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Layout
{
@@ -11,6 +12,13 @@ namespace Lantean.QBTMudBlade.Layout
[CascadingParameter]
public IEnumerable<Torrent>? Torrents { get; set; }
[CascadingParameter(Name = "SortColumn")]
public string? SortColumn { get; set; }
[CascadingParameter(Name = "SortDirection")]
public SortDirection SortDirection { get; set; }
protected string? SelectedTorrent { get; set; }
protected override void OnParametersSet()

View File

@@ -12,13 +12,21 @@
<CascadingValue Value="Torrents">
<CascadingValue Value="MainData">
<CascadingValue Value="Preferences">
<CascadingValue Value="CategoryChanged" Name="CategoryChanged">
<CascadingValue Value="StatusChanged" Name="StatusChanged">
<CascadingValue Value="TagChanged" Name="TagChanged">
<CascadingValue Value="TrackerChanged" Name="TrackerChanged">
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
@Body
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
<CascadingValue Value="SortColumn" Name="SortColumn">
<CascadingValue Value="SortDirectionChanged" Name="SortDirectionChanged">
<CascadingValue Value="SortDirection" Name="SortDirection">
<CascadingValue Value="CategoryChanged" Name="CategoryChanged">
<CascadingValue Value="StatusChanged" Name="StatusChanged">
<CascadingValue Value="TagChanged" Name="TagChanged">
<CascadingValue Value="TrackerChanged" Name="TrackerChanged">
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
@@ -29,28 +37,28 @@
<MudAppBar Bottom="true" Style="background-color: var(--mud-palette-dark-lighten);">
@if (MainData?.LostConnection == true)
{
<MudText Color="Color.Error">qBittorrent client is not reachable</MudText>
<MudText Class="pl-1 pr-1 pt-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="pl-1 pr-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudText Class="pl-1 pr-1 pt-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" />
<MudText Class="pl-1 pr-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudText Class="pl-1 pr-1 pt-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="pl-1 pr-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudIcon Class="pl-1 pr-1 pt-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" />
<MudIcon Class="pl-1 pr-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudIcon Class="pl-1 pr-1 pt-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" />
<MudIcon Class="pl-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Success" />
<MudText Class="pr-1">
<MudIcon Class="pl-1 pt-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Success" />
<MudText Class="pr-1 pt-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
</MudText>
<MudDivider Vertical="true" />
<MudIcon Class="pl-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Info" />
<MudText Class="pr-1">
<MudIcon Class="pl-1 pt-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Info" />
<MudText Class="pr-1 pt-1">
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
</MudText>

View File

@@ -39,6 +39,10 @@ namespace Lantean.QBTMudBlade.Layout
protected QBitTorrentClient.Models.Preferences? Preferences { get; set; }
protected string? SortColumn { get; set; }
protected SortDirection SortDirection { get; set; }
protected string Version { get; set; } = "";
protected string? SearchText { get; set; }
@@ -142,6 +146,10 @@ namespace Lantean.QBTMudBlade.Layout
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
protected EventCallback<SortDirection> SortDirectionChanged => EventCallback.Factory.Create<SortDirection>(this, sortDirection => SortDirection = sortDirection);
protected static (string, Color) GetConnectionIcon(string? status)
{
if (status is null)

View File

@@ -2,35 +2,36 @@
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar Elevation="1">
<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>
}
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
</CascadingValue>
</CascadingValue>
</MudLayout>
</EnhancedErrorBoundary>
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar Elevation="1">
<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>
}
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</MudLayout>

View File

@@ -30,6 +30,10 @@ namespace Lantean.QBTMudBlade.Layout
protected EnhancedErrorBoundary? ErrorBoundary { get; set; }
protected bool IsDarkMode { get; set; }
protected MudThemeProvider MudThemeProvider { get; set; } = default!;
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
{
ReportRate = 50,
@@ -53,10 +57,17 @@ namespace Lantean.QBTMudBlade.Layout
{
if (firstRender)
{
IsDarkMode = await MudThemeProvider.GetSystemPreference();
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
await InvokeAsync(StateHasChanged);
}
}
await base.OnAfterRenderAsync(firstRender);
protected async Task OnSystemPreferenceChanged(bool value)
{
IsDarkMode = value;
await InvokeAsync(StateHasChanged);
}
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)

View File

@@ -1,5 +1,8 @@
namespace Lantean.QBTMudBlade.Models
using System.Diagnostics;
namespace Lantean.QBTMudBlade.Models
{
[DebuggerDisplay("{Name}")]
public class ContentItem
{
public ContentItem(

View File

@@ -2,7 +2,6 @@
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Pages
@@ -48,7 +47,7 @@ namespace Lantean.QBTMudBlade.Pages
return torrent.Name;
}
protected async Task PauseTorrent(MouseEventArgs eventArgs)
protected async Task PauseTorrent()
{
if (Hash is null)
{
@@ -58,7 +57,7 @@ namespace Lantean.QBTMudBlade.Pages
await ApiClient.PauseTorrent(Hash);
}
protected async Task ResumeTorrent(MouseEventArgs eventArgs)
protected async Task ResumeTorrent()
{
if (Hash is null)
{
@@ -68,7 +67,7 @@ namespace Lantean.QBTMudBlade.Pages
await ApiClient.ResumeTorrent(Hash);
}
protected async Task RemoveTorrent(MouseEventArgs eventArgs)
protected async Task RemoveTorrent()
{
if (Hash is null)
{
@@ -83,6 +82,8 @@ namespace Lantean.QBTMudBlade.Pages
}
await ApiClient.DeleteTorrent(Hash, (bool)result.Data);
NavigationManager.NavigateTo("/");
}
protected void NavigateBack()

View File

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

View File

@@ -12,60 +12,22 @@
<MudSpacer />
<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>
<MudTable
Items="OrderedTorrents"
T="Torrent"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Loading="@(Torrents is null)"
SelectOnRowClick="false"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
MultiSelection="true"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChanged"
HorizontalScrollbar="true"
OnRowClick="RowClick"
RowStyleFunc="RowStyle"
Virtualize="true"
Class="torrent-list">
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())
{
var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null;
<col style="@style" />
}
</ColGroup>
<HeaderContent>
@foreach (var column in GetColumns())
{
<MudTh>
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="Torrent" SortDirectionChanged="@(c => SetSort(column.Id, c))">@column.Header</MudTableSortLabel>
}
else
{
@column.Header
}
</MudTh>
}
</HeaderContent>
<RowTemplate>
@foreach (var column in GetColumns())
{
<MudTd DataLabel="@column.Header" Class="@column.Class">
@column.RowTemplate(column.GetRowContext(context))
</MudTd>
}
</RowTemplate>
</MudTable>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable
@ref="Table"
T="Torrent"
ColumnDefinitions="Columns"
ColumnFilter="@(c => c.Id != "#" || Preferences?.QueueingEnabled == true)"
Items="Torrents"
OnRowClick="RowClick"
MultiSelection="true"
SelectedItemsChanged="SelectedItemsChanged"
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChangedHandler"
SortDirectionChanged="SortDirectionChangedHandler"
/>
</MudContainer>
@code {
private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
@@ -76,7 +38,7 @@
{
var value = (float?)context.GetValue();
var color = value < 1 ? Color.Success : Color.Info;
<MudProgressLinear title="Progress" Color="@(color)" Value="@((value ?? 0) * 100)" Class="progress-expand" Size="Size.Large">
<MudProgressLinear title="Progress" Color="@color" Value="@((value ?? 0) * 100)" Class="progress-expand" Size="Size.Large">
@DisplayHelpers.Percentage(value)
</MudProgressLinear>;
};

View File

@@ -1,5 +1,6 @@
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
@@ -10,9 +11,6 @@ namespace Lantean.QBTMudBlade.Pages
{
public partial class TorrentList
{
private const string _columnSelectionStorageKey = "TorrentList.ColumnSelection";
private const string _columnSortStorageKey = "TorrentList.ColumnSort";
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
@@ -31,11 +29,15 @@ namespace Lantean.QBTMudBlade.Pages
[CascadingParameter]
public IEnumerable<Torrent>? Torrents { get; set; }
protected IEnumerable<Torrent>? OrderedTorrents => GetOrderedTorrents();
[CascadingParameter(Name = "SearchTermChanged")]
public EventCallback<string> SearchTermChanged { get; set; }
[CascadingParameter(Name = "SortColumnChanged")]
public EventCallback<string> SortColumnChanged { get; set; }
[CascadingParameter(Name = "SortDirectionChanged")]
public EventCallback<SortDirection> SortDirectionChanged { get; set; }
protected string? SearchText { get; set; }
protected Torrent? SelectedTorrent { get; set; }
@@ -44,51 +46,7 @@ namespace Lantean.QBTMudBlade.Pages
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0 || SelectedTorrent is not null;
protected override async Task OnInitializedAsync()
{
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnSelectionStorageKey);
if (selectedColumns is not null)
{
SelectedColumns = selectedColumns;
}
var columnSort = await LocalStorage.GetItemAsync<Tuple<string, SortDirection>>(_columnSortStorageKey);
if (columnSort is not null)
{
_sortColumn = columnSort.Item1;
_sortDirection = columnSort.Item2;
}
}
protected override void OnParametersSet()
{
if (SelectedColumns.Count == 0)
{
SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet();
if (Preferences?.QueueingEnabled == false)
{
SelectedColumns.Remove("#");
}
}
_sortColumn ??= _columns.First(c => c.Enabled).Id;
if (SelectedTorrent is not null && Torrents is not null && !Torrents.Contains(SelectedTorrent))
{
SelectedTorrent = null;
}
}
private IEnumerable<Torrent>? GetOrderedTorrents()
{
if (Torrents is null)
{
return null;
}
var sortSelector = _columns.Find(c => c.Id == _sortColumn)?.SortSelector;
return Torrents.OrderByDirection(_sortDirection, sortSelector ?? (t => t.Priority));
}
protected DynamicTable<Torrent>? Table { get; set; }
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
{
@@ -99,13 +57,28 @@ namespace Lantean.QBTMudBlade.Pages
}
}
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
{
await SortDirectionChanged.InvokeAsync(sortDirection);
}
protected void SelectedItemChanged(Torrent torrent)
{
SelectedTorrent = torrent;
}
protected async Task SortColumnChangedHandler(string columnId)
{
await SortColumnChanged.InvokeAsync(columnId);
}
protected async Task SearchTextChanged(string text)
{
SearchText = text;
await SearchTermChanged.InvokeAsync(SearchText);
}
protected async Task PauseTorrents(MouseEventArgs eventArgs)
protected async Task PauseTorrents()
{
await ApiClient.PauseTorrents(GetSelectedTorrents());
@@ -113,7 +86,7 @@ namespace Lantean.QBTMudBlade.Pages
await InvokeAsync(StateHasChanged);
}
protected async Task ResumeTorrents(MouseEventArgs eventArgs)
protected async Task ResumeTorrents()
{
await ApiClient.ResumeTorrents(GetSelectedTorrents());
@@ -121,7 +94,7 @@ namespace Lantean.QBTMudBlade.Pages
await InvokeAsync(StateHasChanged);
}
protected async Task RemoveTorrents(MouseEventArgs eventArgs)
protected async Task RemoveTorrents()
{
var reference = await DialogService.ShowAsync<DeleteDialog>("Remove torrent(s)?");
var result = await reference.Result;
@@ -136,44 +109,18 @@ namespace Lantean.QBTMudBlade.Pages
await InvokeAsync(StateHasChanged);
}
protected async Task AddTorrentFile(MouseEventArgs eventArgs)
protected async Task AddTorrentFile()
{
await DialogService.InvokeAddTorrentFileDialog(ApiClient);
}
protected async Task AddTorrentLink(MouseEventArgs eventArgs)
protected async Task AddTorrentLink()
{
await DialogService.InvokeAddTorrentLinkDialog(ApiClient);
}
protected void RowClick(TableRowClickEventArgs<Torrent> eventArgs)
{
if (eventArgs.MouseEventArgs.CtrlKey)
{
if (SelectedItems.Contains(eventArgs.Item))
{
SelectedItems.Remove(eventArgs.Item);
}
else
{
SelectedItems.Add(eventArgs.Item);
}
return;
}
if (SelectedItems.Contains(eventArgs.Item))
{
SelectedItems.Remove(eventArgs.Item);
}
else
{
SelectedItems.Clear();
SelectedItems.Add(eventArgs.Item);
}
SelectedTorrent = eventArgs.Item;
if (eventArgs.MouseEventArgs.Detail > 1)
{
NavigationManager.NavigateTo("/details/" + SelectedTorrent);
@@ -195,29 +142,19 @@ namespace Lantean.QBTMudBlade.Pages
return [];
}
protected void Options(MouseEventArgs eventArgs)
protected void Options()
{
NavigationManager.NavigateTo("/options");
}
protected async Task ColumnOptions()
public async Task ColumnOptions()
{
DialogParameters parameters = new DialogParameters
{
{ "Columns", _columns }
};
var reference = await DialogService.ShowAsync<ColumnOptionsDialog<Torrent>>("ColumnOptions", parameters, DialogHelper.FormDialogOptions);
var result = await reference.Result;
if (result.Canceled)
if (Table is null)
{
return;
}
SelectedColumns = (HashSet<string>)result.Data;
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
await Table.ShowColumnOptionsDialog();
}
protected void ShowTorrent()
@@ -229,35 +166,12 @@ namespace Lantean.QBTMudBlade.Pages
NavigationManager.NavigateTo("/details/" + SelectedTorrent.Hash);
}
protected string RowStyle(Torrent torrent, int index)
{
var style = "user-select: none; cursor: pointer;";
if (torrent == SelectedTorrent)
{
style += " background: #D3D3D3";
}
return style;
}
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions;
protected HashSet<string> SelectedColumns { get; set; } = new HashSet<string>();
protected IEnumerable<ColumnDefinition<Torrent>> GetColumns()
{
return _columns.Where(c => SelectedColumns.Contains(c.Id));
}
private async Task SetSort(string columnId, SortDirection sortDirection)
{
_sortColumn = columnId;
_sortDirection = sortDirection;
await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple<string, SortDirection>(columnId, sortDirection));
}
protected List<ColumnDefinition<Torrent>> _columns =
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
[
CreateColumnDefinition("#", t => t.Priority),
CreateColumnDefinition("State Icon", t => t.State, IconColumn),
CreateColumnDefinition("", t => t.State, IconColumn),
CreateColumnDefinition("Name", t => t.Name, width: 200),
CreateColumnDefinition("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
CreateColumnDefinition("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),
@@ -291,9 +205,6 @@ namespace Lantean.QBTMudBlade.Pages
//CreateColumnDefinition("Reannounce In", t => t.Reannounce, enabled: false),
];
private string? _sortColumn;
private SortDirection _sortDirection;
private static ColumnDefinition<Torrent> CreateColumnDefinition(string name, Func<Torrent, object?> selector, RenderFragment<RowContext<Torrent>> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true)
{
var cd = new ColumnDefinition<Torrent>(name, selector, rowTemplate);

View File

@@ -594,26 +594,35 @@ namespace Lantean.QBTMudBlade.Services
var directories = contents.Where(c => c.Value.IsFolder).OrderByDescending(c => c.Value.Level);
foreach (var key in directories.Select(d => d.Key))
foreach (var directory in directories)
{
var directoryContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && !c.Value.IsFolder);
var key = directory.Key;
var level = directory.Value.Level;
var filesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && !c.Value.IsFolder).ToList();
var directoriesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && c.Value.IsFolder && c.Value.Level == level + 1).ToList();
var directoryContents = filesContents.Concat(directoriesContents);
var size = directoryContents.Sum(c => c.Value.Size);
var availability = directoryContents.Average(c => c.Value.Availability);
var downloaded = directoryContents.Sum(c => c.Value.Downloaded);
var progress = (float)downloaded / size;
var content = contents[key];
content.Availability = availability;
content.Size = size;
content.Progress = progress;
if (!contents.TryGetValue(key, out var dir))
{
continue;
}
dir.Availability = availability;
dir.Size = size;
dir.Progress = progress;
var priorities = directoryContents.Select(d => d.Value.Priority).Distinct();
if (priorities.Count() == 1)
{
content.Priority = priorities.First();
dir.Priority = priorities.First();
}
else
{
content.Priority = Priority.Mixed;
dir.Priority = Priority.Mixed;
}
}

View File

@@ -48,6 +48,8 @@ namespace Lantean.QBTMudBlade
public string? Class { get; set; }
public Func<T, string?>? ClassFunc { get; set; }
public bool Enabled { get; set; } = true;
public SortDirection InitialDirection { get; set; } = SortDirection.None;

View File

@@ -154,4 +154,16 @@ td.no-wrap {
overflow: auto;
height: 200px;
width: 100%;
}
.overflow-cell {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.table-select .mud-input-control > .mud-input-control-input-container > div.mud-input.mud-input-text {
margin-top: 0;
margin-left: 5px;
margin-right: 5px;
}