From baa80220e417d256f092dd896cc2f373a8d27809 Mon Sep 17 00:00:00 2001 From: ahjephson Date: Mon, 13 May 2024 12:01:07 +0100 Subject: [PATCH] Fix filter crash on files and add local storage for column sorts and filter nav --- .../Components/EnhancedErrorBoundary.cs | 9 +++ Lantean.QBTMudBlade/Components/FilesTab.razor | 5 +- .../Components/FilesTab.razor.cs | 60 ++++++++++++------- .../Components/FiltersNav.razor.cs | 40 ++++++++++++- Lantean.QBTMudBlade/DisplayHelpers.cs | 15 +++++ .../Filter/FilterExpressionGenerator.cs | 12 ++-- Lantean.QBTMudBlade/Layout/MainLayout.razor | 59 +++++++++--------- Lantean.QBTMudBlade/Pages/Login.razor.cs | 2 +- Lantean.QBTMudBlade/Pages/TorrentList.razor | 2 +- .../Pages/TorrentList.razor.cs | 33 +++++----- 10 files changed, 161 insertions(+), 76 deletions(-) diff --git a/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs b/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs index eb73a16..a6423c3 100644 --- a/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs +++ b/Lantean.QBTMudBlade/Components/EnhancedErrorBoundary.cs @@ -1,6 +1,7 @@ using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Rendering; using System.Collections.ObjectModel; +using System.Runtime.ExceptionServices; namespace Lantean.QBTMudBlade.Components { @@ -13,10 +14,18 @@ namespace Lantean.QBTMudBlade.Components [Parameter] public EventCallback OnClear { get; set; } + [Parameter] + public bool Disabled { get; set; } + protected override Task OnErrorAsync(Exception exception) { _exceptions.Add(exception); + if (Disabled) + { + ExceptionDispatchInfo.Capture(exception).Throw(); + } + return Task.CompletedTask; } diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor b/Lantean.QBTMudBlade/Components/FilesTab.razor index 092fc00..1e4ae26 100644 --- a/Lantean.QBTMudBlade/Components/FilesTab.razor +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor @@ -26,7 +26,8 @@ OnRowClick="RowClick" RowStyleFunc="RowStyle" RowClassFunc="RowClass" - AllowUnsorted="false"> + AllowUnsorted="false" + Virtualize="true"> @foreach (var column in GetColumns()) @@ -41,7 +42,7 @@ @if (column.SortSelector is not null) { - @column.Header + @column.Header } else { diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs index c24ccf7..88bf71b 100644 --- a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs @@ -16,14 +16,15 @@ namespace Lantean.QBTMudBlade.Components { private readonly bool _refreshEnabled = true; - private const string _columnStorageKey = "FilesTab.Columns"; + private const string _columnSelectionStorageKey = "FilesTab.ColumnSelection"; + private const string _columnSortStorageKey = "FilesTab.ColumnSort"; private readonly CancellationTokenSource _timerCancellationToken = new(); private bool _disposedValue; - private Func SortSelector { get; set; } = c => c.Name; + private string? _sortColumn; - private SortDirection SortDirection { get; set; } = SortDirection.Ascending; + private SortDirection _sortDirection = SortDirection.Ascending; [Parameter] public bool Active { get; set; } @@ -55,6 +56,7 @@ namespace Lantean.QBTMudBlade.Components protected HashSet SelectedItems { get; set; } = []; protected List> _columns = []; + private List>? _filterDefinitions; protected ContentItem? SelectedItem { get; set; } @@ -80,18 +82,18 @@ namespace Lantean.QBTMudBlade.Components protected override async Task OnInitializedAsync() { - if (!await LocalStorage.ContainKeyAsync(_columnStorageKey)) + var selectedColumns = await LocalStorage.GetItemAsync>(_columnSelectionStorageKey); + if (selectedColumns is not null) { - return; + SelectedColumns = selectedColumns; } - var selectedColumns = await LocalStorage.GetItemAsync>(_columnStorageKey); - if (selectedColumns is null) + var columnSort = await LocalStorage.GetItemAsync>(_columnSortStorageKey); + if (columnSort is not null) { - return; + _sortColumn = columnSort.Item1; + _sortDirection = columnSort.Item2; } - - SelectedColumns = selectedColumns; } protected IEnumerable> GetColumns() @@ -116,14 +118,14 @@ namespace Lantean.QBTMudBlade.Components SelectedColumns = (HashSet)result.Data; - await LocalStorage.SetItemAsync(_columnStorageKey, SelectedColumns); + await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns); } protected async Task ShowFilterDialog() { var parameters = new DialogParameters { - { nameof(FilterOptionsDialog.FilterDefinitions), Filters }, + { nameof(FilterOptionsDialog.FilterDefinitions), _filterDefinitions }, }; var result = await DialogService.ShowAsync>("Filters", parameters, DialogHelper.FormDialogOptions); @@ -134,14 +136,15 @@ namespace Lantean.QBTMudBlade.Components return; } - var filterDefinitions = (List>?)dialogResult.Data; - if (filterDefinitions is null) + _filterDefinitions = (List>?)dialogResult.Data; + if (_filterDefinitions is null) { + Filters = null; return; } var filters = new List>(); - foreach (var filterDefinition in filterDefinitions) + foreach (var filterDefinition in _filterDefinitions) { var expression = Filter.FilterExpressionGenerator.GenerateExpression(filterDefinition, false); filters.Add(expression.Compile()); @@ -154,6 +157,11 @@ namespace Lantean.QBTMudBlade.Components { Filters = null; await InvokeAsync(StateHasChanged); + if (FileList is null) + { + return; + } + SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet(); } public async ValueTask DisposeAsync() @@ -378,10 +386,12 @@ namespace Lantean.QBTMudBlade.Components } } - private void SetSort(Func sortSelector, SortDirection sortDirection) + private async Task SetSort(string columnId, SortDirection sortDirection) { - SortSelector = sortSelector; - SortDirection = sortDirection; + _sortColumn = columnId; + _sortDirection = sortDirection; + + await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple(columnId, sortDirection)); } protected void ToggleNode(ContentItem contentItem, MouseEventArgs args) @@ -411,11 +421,19 @@ namespace Lantean.QBTMudBlade.Components return Files.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder); } + private Func GetSortSelector() + { + var sortSelector = _columns.Find(c => c.Id == _sortColumn)?.SortSelector; + + return sortSelector ?? (i => i.Name); + } + private IEnumerable GetDescendants(ContentItem folder, int level) { level++; var descendantsKey = folder.GetDescendantsKey(level); - foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey)).OrderByDirection(SortDirection, SortSelector)) + + foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey)).OrderByDirection(_sortDirection, GetSortSelector())) { if (item.IsFolder) { @@ -474,12 +492,12 @@ namespace Lantean.QBTMudBlade.Components // this is a flat file structure if (maxLevel == 0) { - return FileList.Values.Where(FilterContentItem).OrderByDirection(SortDirection, SortSelector).ToList().AsReadOnly(); + return FileList.Values.Where(FilterContentItem).OrderByDirection(_sortDirection, GetSortSelector()).ToList().AsReadOnly(); } var list = new List(); - var folders = FileList.Values.Where(c => c.IsFolder && c.Level == 0).OrderByDirection(SortDirection, SortSelector).ToList(); + var folders = FileList.Values.Where(c => c.IsFolder && c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList(); foreach (var folder in folders) { list.Add(folder); diff --git a/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs b/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs index 4349177..c9cd94c 100644 --- a/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs +++ b/Lantean.QBTMudBlade/Components/FiltersNav.razor.cs @@ -1,4 +1,5 @@ -using Lantean.QBTMudBlade.Models; +using Blazored.LocalStorage; +using Lantean.QBTMudBlade.Models; using Microsoft.AspNetCore.Components; using MudBlazor; @@ -6,6 +7,11 @@ namespace Lantean.QBTMudBlade.Components { public partial class FiltersNav { + private const string _statusSelectionStorageKey = "FiltersNav.Selection.Status"; + private const string _categorySelectionStorageKey = "FiltersNav.Selection.Category"; + private const string _tagSelectionStorageKey = "FiltersNav.Selection.Tag"; + private const string _trackerSelectionStorageKey = "FiltersNav.Selection.Tracker"; + private bool _statusExpanded = true; private bool _categoriesExpanded = true; private bool _tagsExpanded = true; @@ -19,6 +25,9 @@ namespace Lantean.QBTMudBlade.Components protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL; + [Inject] + public ILocalStorageService LocalStorage { get; set; } = default!; + [CascadingParameter] public MainData? MainData { get; set; } @@ -42,28 +51,55 @@ namespace Lantean.QBTMudBlade.Components public Dictionary Statuses => MainData?.StatusState.ToDictionary(d => d.Key, d => d.Value.Count) ?? []; + protected override async Task OnInitializedAsync() + { + if (await LocalStorage.ContainKeyAsync(_statusSelectionStorageKey)) + { + Status = await LocalStorage.GetItemAsStringAsync(_statusSelectionStorageKey) ?? Models.Status.All.ToString(); + } + + if (await LocalStorage.ContainKeyAsync(_categorySelectionStorageKey)) + { + Category = await LocalStorage.GetItemAsStringAsync(_categorySelectionStorageKey) ?? FilterHelper.CATEGORY_ALL; + } + + if (await LocalStorage.ContainKeyAsync(_tagSelectionStorageKey)) + { + Tag = await LocalStorage.GetItemAsStringAsync(_tagSelectionStorageKey) ?? FilterHelper.TAG_ALL; + } + + if (await LocalStorage.ContainKeyAsync(_trackerSelectionStorageKey)) + { + Tracker = await LocalStorage.GetItemAsStringAsync(_trackerSelectionStorageKey) ?? FilterHelper.TRACKER_ALL; + } + } + protected async Task StatusValueChanged(string value) { Status = value; await StatusChanged.InvokeAsync(Enum.Parse(value)); + await LocalStorage.SetItemAsStringAsync(_statusSelectionStorageKey, value); } protected async Task CategoryValueChanged(string value) { Category = value; await CategoryChanged.InvokeAsync(value); + await LocalStorage.SetItemAsStringAsync(_categorySelectionStorageKey, value); } protected async Task TagValueChanged(string value) { Tag = value; await TagChanged.InvokeAsync(value); + await LocalStorage.SetItemAsStringAsync(_tagSelectionStorageKey, value); } protected async Task TrackerValueChanged(string value) { Tracker = value; await TrackerChanged.InvokeAsync(value); + await LocalStorage.SetItemAsStringAsync(_trackerSelectionStorageKey, value); } protected static string GetHostName(string tracker) @@ -78,7 +114,5 @@ namespace Lantean.QBTMudBlade.Components return tracker; } } - - } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/DisplayHelpers.cs b/Lantean.QBTMudBlade/DisplayHelpers.cs index 54be109..85209e3 100644 --- a/Lantean.QBTMudBlade/DisplayHelpers.cs +++ b/Lantean.QBTMudBlade/DisplayHelpers.cs @@ -28,6 +28,11 @@ namespace Lantean.QBTMudBlade return "∞"; } + if (seconds < 60) + { + return "< 1m"; + } + var time = TimeSpan.FromSeconds(seconds.Value); var sb = new StringBuilder(); if (prefix is not null) @@ -85,6 +90,11 @@ namespace Lantean.QBTMudBlade return ""; } + if (size < 0) + { + size = 0; + } + var stringBuilder = new StringBuilder(); if (prefix is not null) { @@ -275,6 +285,11 @@ namespace Lantean.QBTMudBlade return ""; } + if (value < 0) + { + value = 0; + } + if (value == 0) { return "0%"; diff --git a/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs b/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs index e7dcdb9..6a705cd 100644 --- a/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs +++ b/Lantean.QBTMudBlade/Filter/FilterExpressionGenerator.cs @@ -30,17 +30,17 @@ namespace Lantean.QBTMudBlade.Filter return filter.Operator switch { FilterOperator.String.Contains => - propertyExpression.Modify((Expression>)(x => x != null && value != null && x.Contains(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && value != null && ((string)x).Contains(value, stringComparer))), FilterOperator.String.NotContains => - propertyExpression.Modify((Expression>)(x => x != null && value != null && !x.Contains(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && value != null && !((string)x).Contains(value, stringComparer))), FilterOperator.String.Equal => - propertyExpression.Modify((Expression>)(x => x != null && x.Equals(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && ((string)x).Equals(value, stringComparer))), FilterOperator.String.NotEqual => - propertyExpression.Modify((Expression>)(x => x != null && !x.Equals(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && !((string)x).Equals(value, stringComparer))), FilterOperator.String.StartsWith => - propertyExpression.Modify((Expression>)(x => x != null && value != null && x.StartsWith(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && value != null && ((string)x).StartsWith(value, stringComparer))), FilterOperator.String.EndsWith => - propertyExpression.Modify((Expression>)(x => x != null && value != null && x.EndsWith(value, stringComparer))), + propertyExpression.Modify((Expression>)(x => (string?)x != null && value != null && ((string)x).EndsWith(value, stringComparer))), FilterOperator.String.Empty => propertyExpression.Modify((Expression>)(x => string.IsNullOrWhiteSpace(x))), FilterOperator.String.NotEmpty => propertyExpression.Modify((Expression>)(x => !string.IsNullOrWhiteSpace(x))), _ => x => true diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor b/Lantean.QBTMudBlade/Layout/MainLayout.razor index ecea370..ad996a5 100644 --- a/Lantean.QBTMudBlade/Layout/MainLayout.razor +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor @@ -1,33 +1,36 @@ @inherits LayoutComponentBase + - - - + + - qBittorrent Web UI + + + - - - - qBittorrent Web UI - - @if (ErrorBoundary?.Errors.Count > 0) - { - - - - } - @if (ShowMenu) - { - - } - - - - - - @Body - - - \ No newline at end of file +qBittorrent Web UI + + + + + qBittorrent Web UI + + @if (ErrorBoundary?.Errors.Count > 0) + { + + + + } + @if (ShowMenu) + { + + } + + + + + + @Body + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs index 6ffb550..dbe04b7 100644 --- a/Lantean.QBTMudBlade/Pages/Login.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages #if DEBUG protected override async Task OnInitializedAsync() { - await DoLogin("admin", "a8hbfvNP2"); + await DoLogin("admin", "pdqYws6EQ"); } #endif } diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor b/Lantean.QBTMudBlade/Pages/TorrentList.razor index 252f52c..409adba 100644 --- a/Lantean.QBTMudBlade/Pages/TorrentList.razor +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor @@ -48,7 +48,7 @@ @if (column.SortSelector is not null) { - @column.Header + @column.Header } else { diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs index 95d1849..7845bf2 100644 --- a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs @@ -10,7 +10,8 @@ namespace Lantean.QBTMudBlade.Pages { public partial class TorrentList { - private const string _columnStorageKey = "TorrentList.Columns"; + private const string _columnSelectionStorageKey = "TorrentList.ColumnSelection"; + private const string _columnSortStorageKey = "TorrentList.ColumnSort"; [Inject] protected IApiClient ApiClient { get; set; } = default!; @@ -45,18 +46,18 @@ namespace Lantean.QBTMudBlade.Pages protected override async Task OnInitializedAsync() { - if (!await LocalStorage.ContainKeyAsync(_columnStorageKey)) + var selectedColumns = await LocalStorage.GetItemAsync>(_columnSelectionStorageKey); + if (selectedColumns is not null) { - return; + SelectedColumns = selectedColumns; } - var selectedColumns = await LocalStorage.GetItemAsync>(_columnStorageKey); - if (selectedColumns is null) + var columnSort = await LocalStorage.GetItemAsync>(_columnSortStorageKey); + if (columnSort is not null) { - return; + _sortColumn = columnSort.Item1; + _sortDirection = columnSort.Item2; } - - SelectedColumns = selectedColumns; } protected override void OnParametersSet() @@ -69,7 +70,7 @@ namespace Lantean.QBTMudBlade.Pages SelectedColumns.Remove("#"); } } - _sortSelector ??= _columns.First(c => c.Enabled).SortSelector; + _sortColumn ??= _columns.First(c => c.Enabled).Id; if (SelectedTorrent is not null && Torrents is not null && !Torrents.Contains(SelectedTorrent)) { @@ -84,7 +85,9 @@ namespace Lantean.QBTMudBlade.Pages return null; } - return Torrents.OrderByDirection(_sortDirection, _sortSelector ?? (t => t.Priority)); + var sortSelector = _columns.Find(c => c.Id == _sortColumn)?.SortSelector; + + return Torrents.OrderByDirection(_sortDirection, sortSelector ?? (t => t.Priority)); } protected void SelectedItemsChanged(HashSet selectedItems) @@ -214,7 +217,7 @@ namespace Lantean.QBTMudBlade.Pages SelectedColumns = (HashSet)result.Data; - await LocalStorage.SetItemAsync(_columnStorageKey, SelectedColumns); + await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns); } protected void ShowTorrent() @@ -243,10 +246,12 @@ namespace Lantean.QBTMudBlade.Pages return _columns.Where(c => SelectedColumns.Contains(c.Id)); } - private void SetSort(Func sortSelector, SortDirection sortDirection) + private async Task SetSort(string columnId, SortDirection sortDirection) { - _sortSelector = sortSelector; + _sortColumn = columnId; _sortDirection = sortDirection; + + await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple(columnId, sortDirection)); } protected List> _columns = @@ -286,7 +291,7 @@ namespace Lantean.QBTMudBlade.Pages //CreateColumnDefinition("Reannounce In", t => t.Reannounce, enabled: false), ]; - private Func? _sortSelector; + private string? _sortColumn; private SortDirection _sortDirection; private static ColumnDefinition CreateColumnDefinition(string name, Func selector, RenderFragment> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true)