mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-11-02 21:13:15 +00:00
Fix filter crash on files and add local storage for column sorts and filter nav
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,8 @@
|
||||
OnRowClick="RowClick"
|
||||
RowStyleFunc="RowStyle"
|
||||
RowClassFunc="RowClass"
|
||||
AllowUnsorted="false">
|
||||
AllowUnsorted="false"
|
||||
Virtualize="true">
|
||||
<ColGroup>
|
||||
<col style="width: 30px" />
|
||||
@foreach (var column in GetColumns())
|
||||
@@ -41,7 +42,7 @@
|
||||
<MudTh>
|
||||
@if (column.SortSelector is not null)
|
||||
{
|
||||
<MudTableSortLabel T="Torrent" SortDirectionChanged="@(c => SetSort(column.SortSelector, c))" InitialDirection="column.InitialDirection">@column.Header</MudTableSortLabel>
|
||||
<MudTableSortLabel T="ContentItem" SortDirectionChanged="@(c => SetSort(column.Id, c))" InitialDirection="column.InitialDirection">@column.Header</MudTableSortLabel>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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<ContentItem, object?> 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<ContentItem> SelectedItems { get; set; } = [];
|
||||
|
||||
protected List<ColumnDefinition<ContentItem>> _columns = [];
|
||||
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
|
||||
|
||||
protected ContentItem? SelectedItem { get; set; }
|
||||
|
||||
@@ -80,20 +82,20 @@ namespace Lantean.QBTMudBlade.Components
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await LocalStorage.ContainKeyAsync(_columnStorageKey))
|
||||
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnSelectionStorageKey);
|
||||
if (selectedColumns is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnStorageKey);
|
||||
if (selectedColumns is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
@@ -116,14 +118,14 @@ namespace Lantean.QBTMudBlade.Components
|
||||
|
||||
SelectedColumns = (HashSet<string>)result.Data;
|
||||
|
||||
await LocalStorage.SetItemAsync(_columnStorageKey, SelectedColumns);
|
||||
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
|
||||
}
|
||||
|
||||
protected async Task ShowFilterDialog()
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(FilterOptionsDialog<ContentItem>.FilterDefinitions), Filters },
|
||||
{ nameof(FilterOptionsDialog<ContentItem>.FilterDefinitions), _filterDefinitions },
|
||||
};
|
||||
|
||||
var result = await DialogService.ShowAsync<FilterOptionsDialog<ContentItem>>("Filters", parameters, DialogHelper.FormDialogOptions);
|
||||
@@ -134,14 +136,15 @@ namespace Lantean.QBTMudBlade.Components
|
||||
return;
|
||||
}
|
||||
|
||||
var filterDefinitions = (List<PropertyFilterDefinition<ContentItem>>?)dialogResult.Data;
|
||||
if (filterDefinitions is null)
|
||||
_filterDefinitions = (List<PropertyFilterDefinition<ContentItem>>?)dialogResult.Data;
|
||||
if (_filterDefinitions is null)
|
||||
{
|
||||
Filters = null;
|
||||
return;
|
||||
}
|
||||
|
||||
var filters = new List<Func<ContentItem, bool>>();
|
||||
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<ContentItem, object?> 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<string, SortDirection>(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<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)
|
||||
{
|
||||
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<ContentItem>();
|
||||
|
||||
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);
|
||||
|
||||
@@ -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<string, int> 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<Status>(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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -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%";
|
||||
|
||||
@@ -30,17 +30,17 @@ namespace Lantean.QBTMudBlade.Filter
|
||||
return filter.Operator switch
|
||||
{
|
||||
FilterOperator.String.Contains =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.Contains(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && ((string)x).Contains(value, stringComparer))),
|
||||
FilterOperator.String.NotContains =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && !x.Contains(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && !((string)x).Contains(value, stringComparer))),
|
||||
FilterOperator.String.Equal =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && x.Equals(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && ((string)x).Equals(value, stringComparer))),
|
||||
FilterOperator.String.NotEqual =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && !x.Equals(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && !((string)x).Equals(value, stringComparer))),
|
||||
FilterOperator.String.StartsWith =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.StartsWith(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && ((string)x).StartsWith(value, stringComparer))),
|
||||
FilterOperator.String.EndsWith =>
|
||||
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.EndsWith(value, stringComparer))),
|
||||
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && ((string)x).EndsWith(value, stringComparer))),
|
||||
FilterOperator.String.Empty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => string.IsNullOrWhiteSpace(x))),
|
||||
FilterOperator.String.NotEmpty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => !string.IsNullOrWhiteSpace(x))),
|
||||
_ => x => true
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
|
||||
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
|
||||
|
||||
</EnhancedErrorBoundary>
|
||||
|
||||
<MudThemeProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
@@ -30,4 +34,3 @@
|
||||
@Body
|
||||
</CascadingValue>
|
||||
</MudLayout>
|
||||
</EnhancedErrorBoundary>
|
||||
@@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages
|
||||
#if DEBUG
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await DoLogin("admin", "a8hbfvNP2");
|
||||
await DoLogin("admin", "pdqYws6EQ");
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
<MudTh>
|
||||
@if (column.SortSelector is not null)
|
||||
{
|
||||
<MudTableSortLabel T="Torrent" SortDirectionChanged="@(c => SetSort(column.SortSelector, c))">@column.Header</MudTableSortLabel>
|
||||
<MudTableSortLabel T="Torrent" SortDirectionChanged="@(c => SetSort(column.Id, c))">@column.Header</MudTableSortLabel>
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -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,20 +46,20 @@ namespace Lantean.QBTMudBlade.Pages
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await LocalStorage.ContainKeyAsync(_columnStorageKey))
|
||||
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnSelectionStorageKey);
|
||||
if (selectedColumns is not null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var selectedColumns = await LocalStorage.GetItemAsync<HashSet<string>>(_columnStorageKey);
|
||||
if (selectedColumns is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -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<Torrent> selectedItems)
|
||||
@@ -214,7 +217,7 @@ namespace Lantean.QBTMudBlade.Pages
|
||||
|
||||
SelectedColumns = (HashSet<string>)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<Torrent, object?> sortSelector, SortDirection sortDirection)
|
||||
private async Task SetSort(string columnId, SortDirection sortDirection)
|
||||
{
|
||||
_sortSelector = sortSelector;
|
||||
_sortColumn = columnId;
|
||||
_sortDirection = sortDirection;
|
||||
|
||||
await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple<string, SortDirection>(columnId, sortDirection));
|
||||
}
|
||||
|
||||
protected List<ColumnDefinition<Torrent>> _columns =
|
||||
@@ -286,7 +291,7 @@ namespace Lantean.QBTMudBlade.Pages
|
||||
//CreateColumnDefinition("Reannounce In", t => t.Reannounce, enabled: false),
|
||||
];
|
||||
|
||||
private Func<Torrent, object?>? _sortSelector;
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user