mirror of
				https://github.com/lantean-code/qbtmud.git
				synced 2025-11-03 21:43:19 +00:00 
			
		
		
		
	Compare commits
	
		
			29 Commits
		
	
	
		
			feature/fi
			...
			develop
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					fca17edfd1 | ||
| 
						 | 
					d8535fa262 | ||
| 
						 | 
					1c6bfed6ee | ||
| 
						 | 
					281caf8026 | ||
| 
						 | 
					ff905e7cac | ||
| 
						 | 
					cb80dd0d6b | ||
| 
						 | 
					9113fb90ee | ||
| 
						 | 
					d8b4e932d1 | ||
| 
						 | 
					3d0d211d10 | ||
| 
						 | 
					7db4f2f78d | ||
| 
						 | 
					1f606b4449 | ||
| 
						 | 
					88d66b4887 | ||
| 
						 | 
					2ad7be1073 | ||
| 
						 | 
					300e81345c | ||
| 
						 | 
					9d8d84168e | ||
| 
						 | 
					bb66b97f45 | ||
| 
						 | 
					4824037ba7 | ||
| 
						 | 
					1f9b631a36 | ||
| 
						 | 
					2c744cd972 | ||
| 
						 | 
					b02bb7cfae | ||
| 
						 | 
					e4dac8556e | ||
| 
						 | 
					a9a8a4eba8 | ||
| 
						 | 
					bb524450f0 | ||
| 
						 | 
					d4ac79af00 | ||
| 
						 | 
					7370d73c59 | ||
| 
						 | 
					8796cc0f24 | ||
| 
						 | 
					b24ae440d4 | ||
| 
						 | 
					bb90ce5216 | ||
| 
						 | 
					4eaa46b2b3 | 
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -361,3 +361,4 @@ MigrationBackup/
 | 
			
		||||
 | 
			
		||||
# Fody - auto-generated XML schema
 | 
			
		||||
FodyWeavers.xsd
 | 
			
		||||
/output
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBitTorrentClient.Models;
 | 
			
		||||
using System.Linq.Expressions;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        protected HashSet<string> Tags { get; } = [];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class AddTorrentFileDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        protected IKeyboardService KeyboardService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Url { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBTMud.Models;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
{
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class AddTrackerDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        protected HashSet<string> Trackers { get; } = [];
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        private string _savePath = string.Empty;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Inject]
 | 
			
		||||
        protected IApiClient ApiClient { get; set; } = default!;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class ConfirmDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string Content { get; set; } = default!;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class DeleteDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public int Count { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class ExceptionDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public Exception? Exception { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        protected IReadOnlyList<PropertyInfo> Columns => _properties;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public IEnumerable<string> Hashes { get; set; } = [];
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public IEnumerable<string> Hashes { get; set; } = [];
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class MultipleFieldDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string Label { get; set; } = default!;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class NumericFieldDialog<T> where T : struct, INumber<T>
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Label { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        protected ILocalStorageService LocalStorage { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Hash { get; set; }
 | 
			
		||||
@@ -426,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
            {
 | 
			
		||||
                await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
 | 
			
		||||
            }
 | 
			
		||||
            
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnInitializedAsync()
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
        private readonly List<string> _unsavedRuleNames = [];
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Inject]
 | 
			
		||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,20 +0,0 @@
 | 
			
		||||
<MudDialog>
 | 
			
		||||
    <DialogContent>
 | 
			
		||||
        <MudGrid>
 | 
			
		||||
            <MudItem xs="12">
 | 
			
		||||
                <MudList T="string">
 | 
			
		||||
                    <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 plugin in Plugins)
 | 
			
		||||
                    {
 | 
			
		||||
                        var pluginRef = plugin;
 | 
			
		||||
                        <MudListItem Icon="@GetIcon(pluginRef.FullName)" IconColor="Color.Default" OnClick="@(e => SetPlugin(pluginRef))">@pluginRef.Name</MudListItem>
 | 
			
		||||
                    }
 | 
			
		||||
                </MudList>
 | 
			
		||||
            </MudItem>
 | 
			
		||||
        </MudGrid>
 | 
			
		||||
    </DialogContent>
 | 
			
		||||
    <DialogActions>
 | 
			
		||||
    </DialogActions>
 | 
			
		||||
</MudDialog>
 | 
			
		||||
@@ -1,72 +0,0 @@
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBitTorrentClient.Models;
 | 
			
		||||
using Lantean.QBTMud.Helpers;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
{
 | 
			
		||||
    public partial class SearchPluginsDialog
 | 
			
		||||
    {
 | 
			
		||||
        [Inject]
 | 
			
		||||
        protected IApiClient ApiClient { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Inject]
 | 
			
		||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        protected HashSet<SearchPlugin> Plugins { get; set; } = [];
 | 
			
		||||
 | 
			
		||||
        protected IList<string> TorrentCategories { get; private set; } = [];
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnInitializedAsync()
 | 
			
		||||
        {
 | 
			
		||||
            Plugins = [.. (await ApiClient.GetSearchPlugins())];
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected string GetIcon(string tag)
 | 
			
		||||
        {
 | 
			
		||||
            return Icons.Material.Filled.PlusOne;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task SetPlugin(QBitTorrentClient.Models.SearchPlugin plugin)
 | 
			
		||||
        {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            await InvokeAsync(StateHasChanged);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task AddCategory()
 | 
			
		||||
        {
 | 
			
		||||
            var addedCategoy = await DialogService.InvokeAddCategoryDialog(ApiClient);
 | 
			
		||||
            if (addedCategoy is null)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ApiClient.SetTorrentCategory(addedCategoy, Hashes);
 | 
			
		||||
            Plugins.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();
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class ShareRatioDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Label { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class SliderFieldDialog<T> where T : struct, INumber<T>
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Label { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class StringFieldDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public string? Label { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class SubMenuDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public UIAction? ParentAction { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
			
		||||
    public partial class TorrentOptionsDialog
 | 
			
		||||
    {
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [EditorRequired]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +1,49 @@
 | 
			
		||||
<ContextMenu @ref="ContextMenu" Dense="true">
 | 
			
		||||
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
 | 
			
		||||
        <MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
 | 
			
		||||
        <MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
 | 
			
		||||
        <MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
 | 
			
		||||
    </MudMenu>
 | 
			
		||||
    <MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
 | 
			
		||||
        <MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
 | 
			
		||||
        <MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
 | 
			
		||||
        <MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
 | 
			
		||||
    </MudMenu>
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
 | 
			
		||||
    <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>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
 | 
			
		||||
                <MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
 | 
			
		||||
                <MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
 | 
			
		||||
                <MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
 | 
			
		||||
            </MudMenu>
 | 
			
		||||
            <MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
 | 
			
		||||
                <MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
 | 
			
		||||
                <MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
 | 
			
		||||
                <MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
 | 
			
		||||
            </MudMenu>
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
 | 
			
		||||
            <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>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable
 | 
			
		||||
            @ref="Table"
 | 
			
		||||
            T="ContentItem" 
 | 
			
		||||
            ColumnDefinitions="Columns" 
 | 
			
		||||
            Items="Files" 
 | 
			
		||||
            MultiSelection="false"
 | 
			
		||||
            SelectOnRowClick="true"
 | 
			
		||||
            PreSorted="true"
 | 
			
		||||
            SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
            SortColumnChanged="SortColumnChanged"
 | 
			
		||||
            SortDirectionChanged="SortDirectionChanged"
 | 
			
		||||
            OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
            OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
            Class="file-list content-panel__table"
 | 
			
		||||
        />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<DynamicTable
 | 
			
		||||
    @ref="Table"
 | 
			
		||||
    T="ContentItem" 
 | 
			
		||||
    ColumnDefinitions="Columns" 
 | 
			
		||||
    Items="Files" 
 | 
			
		||||
    MultiSelection="false"
 | 
			
		||||
    SelectOnRowClick="true"
 | 
			
		||||
    PreSorted="true"
 | 
			
		||||
    SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
    SortColumnChanged="SortColumnChanged"
 | 
			
		||||
    SortDirectionChanged="SortDirectionChanged"
 | 
			
		||||
    OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
    OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
    Class="file-list"
 | 
			
		||||
/>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private RenderFragment<RowContext<ContentItem>> NameColumn
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
        private readonly CancellationTokenSource _timerCancellationToken = new();
 | 
			
		||||
        private bool _disposedValue;
 | 
			
		||||
        private static readonly ReadOnlyCollection<ContentItem> EmptyContentItems = new ReadOnlyCollection<ContentItem>(Array.Empty<ContentItem>());
 | 
			
		||||
        private ReadOnlyCollection<ContentItem> _visibleFiles = EmptyContentItems;
 | 
			
		||||
        private bool _filesDirty = true;
 | 
			
		||||
 | 
			
		||||
        private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
 | 
			
		||||
        private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
 | 
			
		||||
@@ -65,7 +68,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
        private DynamicTable<ContentItem>? Table { get; set; }
 | 
			
		||||
 | 
			
		||||
        private ContextMenu? ContextMenu { get; set; }
 | 
			
		||||
        private MudMenu? ContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        public FilesTab()
 | 
			
		||||
        {
 | 
			
		||||
@@ -102,6 +105,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            if (_filterDefinitions is null)
 | 
			
		||||
            {
 | 
			
		||||
                Filters = null;
 | 
			
		||||
                MarkFilesDirty();
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -113,11 +117,13 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Filters = filters;
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void RemoveFilter()
 | 
			
		||||
        {
 | 
			
		||||
            Filters = null;
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public async ValueTask DisposeAsync()
 | 
			
		||||
@@ -157,6 +163,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
        protected void SearchTextChanged(string value)
 | 
			
		||||
        {
 | 
			
		||||
            SearchText = value;
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
 | 
			
		||||
@@ -178,7 +185,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.OpenMenuAsync(eventArgs);
 | 
			
		||||
            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
@@ -197,6 +206,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            {
 | 
			
		||||
                while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
 | 
			
		||||
                {
 | 
			
		||||
                    var hasUpdates = false;
 | 
			
		||||
                    if (Active && Hash is not null)
 | 
			
		||||
                    {
 | 
			
		||||
                        IReadOnlyList<QBitTorrentClient.Models.FileData> files;
 | 
			
		||||
@@ -213,14 +223,20 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
                        if (FileList is null)
 | 
			
		||||
                        {
 | 
			
		||||
                            FileList = DataManager.CreateContentsList(files);
 | 
			
		||||
                            hasUpdates = true;
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            DataManager.MergeContentsList(files, FileList);
 | 
			
		||||
                            hasUpdates = DataManager.MergeContentsList(files, FileList);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await InvokeAsync(StateHasChanged);
 | 
			
		||||
                    if (hasUpdates)
 | 
			
		||||
                    {
 | 
			
		||||
                        MarkFilesDirty();
 | 
			
		||||
                        PruneSelectionIfMissing();
 | 
			
		||||
                        await InvokeAsync(StateHasChanged);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
@@ -246,6 +262,8 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
            var contents = await ApiClient.GetTorrentContents(Hash);
 | 
			
		||||
            FileList = DataManager.CreateContentsList(contents);
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
            PruneSelectionIfMissing();
 | 
			
		||||
 | 
			
		||||
            var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
 | 
			
		||||
            if (expandedNodes is not null)
 | 
			
		||||
@@ -256,6 +274,8 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            {
 | 
			
		||||
                ExpandedNodes.Clear();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
 | 
			
		||||
@@ -320,11 +340,13 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
        protected void SortColumnChanged(string sortColumn)
 | 
			
		||||
        {
 | 
			
		||||
            _sortColumn = sortColumn;
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void SortDirectionChanged(SortDirection sortDirection)
 | 
			
		||||
        {
 | 
			
		||||
            _sortDirection = sortDirection;
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void SelectedItemChanged(ContentItem item)
 | 
			
		||||
@@ -343,6 +365,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
                ExpandedNodes.Add(contentItem.Name);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            MarkFilesDirty();
 | 
			
		||||
            await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -368,44 +391,6 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        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) && f.Level == level).OrderByDirection(_sortDirection, GetSortSelector()))
 | 
			
		||||
            {
 | 
			
		||||
                if (item.IsFolder)
 | 
			
		||||
                {
 | 
			
		||||
                    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)
 | 
			
		||||
                    {
 | 
			
		||||
                        yield return descendant;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (FilterContentItem(item))
 | 
			
		||||
                    {
 | 
			
		||||
                        yield return item;
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private bool FilterContentItem(ContentItem item)
 | 
			
		||||
        {
 | 
			
		||||
            if (Filters is not null)
 | 
			
		||||
@@ -429,38 +414,130 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ReadOnlyCollection<ContentItem> GetFiles()
 | 
			
		||||
        {
 | 
			
		||||
            if (!_filesDirty)
 | 
			
		||||
            {
 | 
			
		||||
                return _visibleFiles;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _visibleFiles = BuildVisibleFiles();
 | 
			
		||||
            _filesDirty = false;
 | 
			
		||||
 | 
			
		||||
            return _visibleFiles;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private ReadOnlyCollection<ContentItem> BuildVisibleFiles()
 | 
			
		||||
        {
 | 
			
		||||
            if (FileList is null || FileList.Values.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                return new ReadOnlyCollection<ContentItem>([]);
 | 
			
		||||
                return EmptyContentItems;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var maxLevel = FileList.Values.Max(f => f.Level);
 | 
			
		||||
            // this is a flat file structure
 | 
			
		||||
            if (maxLevel == 0)
 | 
			
		||||
            var lookup = BuildChildrenLookup();
 | 
			
		||||
            if (!lookup.TryGetValue(string.Empty, out var roots))
 | 
			
		||||
            {
 | 
			
		||||
                return FileList.Values.Where(FilterContentItem).OrderByDirection(_sortDirection, GetSortSelector()).ToList().AsReadOnly();
 | 
			
		||||
                return EmptyContentItems;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var list = new List<ContentItem>();
 | 
			
		||||
            var sortSelector = GetSortSelector();
 | 
			
		||||
            var orderedRoots = roots.OrderByDirection(_sortDirection, sortSelector).ToList();
 | 
			
		||||
            var result = new List<ContentItem>(FileList.Values.Count);
 | 
			
		||||
 | 
			
		||||
            var rootItems = FileList.Values.Where(c => c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
 | 
			
		||||
            foreach (var item in rootItems)
 | 
			
		||||
            foreach (var item in orderedRoots)
 | 
			
		||||
            {
 | 
			
		||||
                list.Add(item);
 | 
			
		||||
 | 
			
		||||
                if (item.IsFolder && ExpandedNodes.Contains(item.Name))
 | 
			
		||||
                if (item.IsFolder)
 | 
			
		||||
                {
 | 
			
		||||
                    var level = 0;
 | 
			
		||||
                    var descendants = GetChildren(item, level);
 | 
			
		||||
                    foreach (var descendant in descendants)
 | 
			
		||||
                    result.Add(item);
 | 
			
		||||
 | 
			
		||||
                    if (!ExpandedNodes.Contains(item.Name))
 | 
			
		||||
                    {
 | 
			
		||||
                        list.Add(descendant);
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var descendants = GetVisibleDescendants(item, lookup, sortSelector);
 | 
			
		||||
                    result.AddRange(descendants);
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
                {
 | 
			
		||||
                    if (FilterContentItem(item))
 | 
			
		||||
                    {
 | 
			
		||||
                        result.Add(item);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return list.AsReadOnly();
 | 
			
		||||
            return new ReadOnlyCollection<ContentItem>(result);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private Dictionary<string, List<ContentItem>> BuildChildrenLookup()
 | 
			
		||||
        {
 | 
			
		||||
            var lookup = new Dictionary<string, List<ContentItem>>(FileList!.Count);
 | 
			
		||||
 | 
			
		||||
            foreach (var item in FileList!.Values)
 | 
			
		||||
            {
 | 
			
		||||
                var parentPath = item.Level == 0 ? string.Empty : item.Name.GetDirectoryPath();
 | 
			
		||||
                if (!lookup.TryGetValue(parentPath, out var children))
 | 
			
		||||
                {
 | 
			
		||||
                    children = [];
 | 
			
		||||
                    lookup[parentPath] = children;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                children.Add(item);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return lookup;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private List<ContentItem> GetVisibleDescendants(ContentItem folder, Dictionary<string, List<ContentItem>> lookup, Func<ContentItem, object?> sortSelector)
 | 
			
		||||
        {
 | 
			
		||||
            if (!lookup.TryGetValue(folder.Name, out var children))
 | 
			
		||||
            {
 | 
			
		||||
                return [];
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var orderedChildren = children.OrderByDirection(_sortDirection, sortSelector).ToList();
 | 
			
		||||
            var visible = new List<ContentItem>();
 | 
			
		||||
 | 
			
		||||
            foreach (var child in orderedChildren)
 | 
			
		||||
            {
 | 
			
		||||
                if (child.IsFolder)
 | 
			
		||||
                {
 | 
			
		||||
                    var descendants = GetVisibleDescendants(child, lookup, sortSelector);
 | 
			
		||||
                    if (descendants.Count != 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        visible.Add(child);
 | 
			
		||||
 | 
			
		||||
                        if (ExpandedNodes.Contains(child.Name))
 | 
			
		||||
                        {
 | 
			
		||||
                            visible.AddRange(descendants);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                else if (FilterContentItem(child))
 | 
			
		||||
                {
 | 
			
		||||
                    visible.Add(child);
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return visible;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void MarkFilesDirty()
 | 
			
		||||
        {
 | 
			
		||||
            _filesDirty = true;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void PruneSelectionIfMissing()
 | 
			
		||||
        {
 | 
			
		||||
            if (SelectedItem is not null && (FileList is null || !FileList.ContainsKey(SelectedItem.Name)))
 | 
			
		||||
            {
 | 
			
		||||
                SelectedItem = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (ContextMenuItem is not null && (FileList is null || !FileList.ContainsKey(ContextMenuItem.Name)))
 | 
			
		||||
            {
 | 
			
		||||
                ContextMenuItem = null;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task DoNotDownloadLessThan100PercentAvailability()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60"> 
 | 
			
		||||
<MudMenu @ref="StatusContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable"> 
 | 
			
		||||
    @TorrentControls(_statusType)
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<ContextMenu @ref="CategoryContextMenu" Dense="true" AdjustmentY="-60">
 | 
			
		||||
<MudMenu @ref="CategoryContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
 | 
			
		||||
    @if (IsCategoryTarget)
 | 
			
		||||
    {
 | 
			
		||||
@@ -12,9 +12,9 @@
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
 | 
			
		||||
    <MudDivider />
 | 
			
		||||
    @TorrentControls(_categoryType)
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<ContextMenu @ref="TagContextMenu" Dense="true" AdjustmentY="-60">
 | 
			
		||||
<MudMenu @ref="TagContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
 | 
			
		||||
    @if (IsTagTarget)
 | 
			
		||||
    {
 | 
			
		||||
@@ -23,13 +23,13 @@
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
 | 
			
		||||
    <MudDivider />
 | 
			
		||||
    @TorrentControls(_tagType)
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<ContextMenu @ref="TrackerContextMenu" Dense="true" AdjustmentY="-60">
 | 
			
		||||
<MudMenu @ref="TrackerContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
 | 
			
		||||
    <MudDivider />
 | 
			
		||||
    @TorrentControls(_trackerType)
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<MudNavMenu Dense="true">
 | 
			
		||||
    <MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
using Blazored.LocalStorage;
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBTMud.Components.UI;
 | 
			
		||||
using Lantean.QBTMud.Helpers;
 | 
			
		||||
using Lantean.QBTMud.Models;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
@@ -69,13 +68,13 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
        protected Dictionary<string, int> Statuses => GetStatuses();
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? StatusContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? StatusContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? CategoryContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? CategoryContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? TagContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? TagContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? TrackerContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? TrackerContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected string? ContextMenuStatus { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -154,7 +153,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
            ContextMenuStatus = value;
 | 
			
		||||
 | 
			
		||||
            return StatusContextMenu.OpenMenuAsync(args);
 | 
			
		||||
            var normalizedArgs = args.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            return StatusContextMenu.OpenMenuAsync(normalizedArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task CategoryValueChanged(string value)
 | 
			
		||||
@@ -192,7 +193,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
 | 
			
		||||
            ContextMenuCategory = value;
 | 
			
		||||
 | 
			
		||||
            return CategoryContextMenu.OpenMenuAsync(args);
 | 
			
		||||
            var normalizedArgs = args.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            return CategoryContextMenu.OpenMenuAsync(normalizedArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task TagValueChanged(string value)
 | 
			
		||||
@@ -230,7 +233,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
            IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
 | 
			
		||||
            ContextMenuTag = value;
 | 
			
		||||
 | 
			
		||||
            return TagContextMenu.OpenMenuAsync(args);
 | 
			
		||||
            var normalizedArgs = args.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            return TagContextMenu.OpenMenuAsync(normalizedArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task TrackerValueChanged(string value)
 | 
			
		||||
@@ -267,7 +272,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
            ContextMenuTracker = value;
 | 
			
		||||
 | 
			
		||||
            return TrackerContextMenu.OpenMenuAsync(args);
 | 
			
		||||
            var normalizedArgs = args.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            return TrackerContextMenu.OpenMenuAsync(normalizedArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task AddCategory()
 | 
			
		||||
 
 | 
			
		||||
@@ -92,7 +92,9 @@
 | 
			
		||||
                <FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
 | 
			
		||||
            </MudItem>
 | 
			
		||||
            <MudItem xs="9">
 | 
			
		||||
                <MudNumericField T="int" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged" Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined" Validation="MaxRatioValidation" />
 | 
			
		||||
                <MudNumericField T="float" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged"
 | 
			
		||||
                    Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined"
 | 
			
		||||
                    Validation="MaxRatioValidation" />
 | 
			
		||||
            </MudItem>
 | 
			
		||||
            <MudItem xs="3">
 | 
			
		||||
                <FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
 | 
			
		||||
 
 | 
			
		||||
@@ -17,7 +17,7 @@
 | 
			
		||||
        protected int SlowTorrentUlRateThreshold { get; private set; }
 | 
			
		||||
        protected int SlowTorrentInactiveTimer { get; private set; }
 | 
			
		||||
        protected bool MaxRatioEnabled { get; private set; }
 | 
			
		||||
        protected int MaxRatio { get; private set; }
 | 
			
		||||
        protected float MaxRatio { get; private set; }
 | 
			
		||||
        protected bool MaxSeedingTimeEnabled { get; private set; }
 | 
			
		||||
        protected int MaxSeedingTime { get; private set; }
 | 
			
		||||
        protected int MaxRatioAct { get; private set; }
 | 
			
		||||
@@ -275,7 +275,7 @@
 | 
			
		||||
            await PreferencesChanged.InvokeAsync(UpdatePreferences);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task MaxRatioChanged(int value)
 | 
			
		||||
        protected async Task MaxRatioChanged(float value)
 | 
			
		||||
        {
 | 
			
		||||
            MaxRatio = value;
 | 
			
		||||
            UpdatePreferences.MaxRatio = value;
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,7 @@
 | 
			
		||||
    <MudCardContent Class="pt-0">
 | 
			
		||||
        <MudGrid>
 | 
			
		||||
            <MudItem xs="12">
 | 
			
		||||
                <MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" Variant="Variant.Outlined">
 | 
			
		||||
                <MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoTmmEnabledChanged" Variant="Variant.Outlined">
 | 
			
		||||
                    <MudSelectItem Value="false">Manual</MudSelectItem>
 | 
			
		||||
                    <MudSelectItem Value="true">Automatic</MudSelectItem>
 | 
			
		||||
                </MudSelect>
 | 
			
		||||
 
 | 
			
		||||
@@ -1,24 +1,30 @@
 | 
			
		||||
<ContextMenu @ref="ContextMenu" Dense="true">
 | 
			
		||||
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
 | 
			
		||||
    @if (ContextMenuItem is not null)
 | 
			
		||||
    {
 | 
			
		||||
        <MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
 | 
			
		||||
    }
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<DynamicTable T="Peer"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Peers"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="true"
 | 
			
		||||
              OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
              OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
              SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
              Class="details-list" />
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable T="Peer"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Peers"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="true"
 | 
			
		||||
                      OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
                      OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
                      SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
                      Class="details-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
        protected Peer? SelectedItem { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? ContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? ContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected DynamicTable<Peer>? Table { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -153,7 +153,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.ToggleMenuAsync(eventArgs);
 | 
			
		||||
            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void SelectedItemChanged(Peer peer)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
<ContextMenu @ref="ContextMenu" Dense="true">
 | 
			
		||||
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
 | 
			
		||||
    @if (ContextMenuItem is not null)
 | 
			
		||||
    {
 | 
			
		||||
@@ -6,27 +6,33 @@
 | 
			
		||||
        <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
 | 
			
		||||
        <MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
 | 
			
		||||
    }
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton>
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton>
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="Lantean.QBitTorrentClient.Models.TorrentTracker"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Trackers"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              PreSorted="true"
 | 
			
		||||
              SortDirectionChanged="SortDirectionChanged"
 | 
			
		||||
              SortColumnChanged="SortColumnChanged"
 | 
			
		||||
              OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
              OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
              SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
              Class="file-list" />
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="Lantean.QBitTorrentClient.Models.TorrentTracker"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Trackers"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      PreSorted="true"
 | 
			
		||||
                      SortDirectionChanged="SortDirectionChanged"
 | 
			
		||||
                      SortColumnChanged="SortColumnChanged"
 | 
			
		||||
                      OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
                      OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
                      SelectedItemChanged="SelectedItemChanged"
 | 
			
		||||
                      Class="file-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
 | 
			
		||||
        protected TorrentTracker? SelectedItem { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? ContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? ContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected DynamicTable<TorrentTracker>? Table { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -148,7 +148,9 @@ namespace Lantean.QBTMud.Components
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.ToggleMenuAsync(eventArgs);
 | 
			
		||||
            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void SelectedItemChanged(TorrentTracker torrentTracker)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,26 +0,0 @@
 | 
			
		||||
@inherits MudComponentBase
 | 
			
		||||
 | 
			
		||||
<MudMenu @ref="FakeMenu" Style="display: none" OpenChanged="FakeOpenChanged"></MudMenu>
 | 
			
		||||
 | 
			
		||||
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
 | 
			
		||||
<MudPopover tracker="@Id"
 | 
			
		||||
            Open="@_open"
 | 
			
		||||
            Class="unselectable"
 | 
			
		||||
            MaxHeight="@MaxHeight"
 | 
			
		||||
            AnchorOrigin="@AnchorOrigin"
 | 
			
		||||
            TransformOrigin="@TransformOrigin"
 | 
			
		||||
            RelativeWidth="@RelativeWidth"
 | 
			
		||||
            OverflowBehavior="OverflowBehavior.FlipAlways"
 | 
			
		||||
            Style="@_popoverStyle"
 | 
			
		||||
            @ontouchend:preventDefault>
 | 
			
		||||
    <CascadingValue Value="@(FakeMenu)">
 | 
			
		||||
        @if (_showChildren)
 | 
			
		||||
        {
 | 
			
		||||
            <MudList T="object" Class="unselectable"  Dense="@Dense">
 | 
			
		||||
                @ChildContent
 | 
			
		||||
            </MudList>
 | 
			
		||||
        }
 | 
			
		||||
    </CascadingValue>
 | 
			
		||||
</MudPopover>
 | 
			
		||||
 | 
			
		||||
<MudOverlay Visible="@(_open)" LockScroll="@LockScroll" AutoClose="true" OnClosed="@CloseMenuAsync" />
 | 
			
		||||
@@ -1,290 +0,0 @@
 | 
			
		||||
using Lantean.QBTMud.Interop;
 | 
			
		||||
using Microsoft.AspNetCore.Components;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Web;
 | 
			
		||||
using Microsoft.JSInterop;
 | 
			
		||||
using MudBlazor;
 | 
			
		||||
using MudBlazor.Utilities;
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
{
 | 
			
		||||
    public partial class ContextMenu : MudComponentBase
 | 
			
		||||
    {
 | 
			
		||||
        private bool _open;
 | 
			
		||||
        private bool _showChildren;
 | 
			
		||||
        private string? _popoverStyle;
 | 
			
		||||
        private string? _id;
 | 
			
		||||
 | 
			
		||||
        private double _x;
 | 
			
		||||
        private double _y;
 | 
			
		||||
        private bool _isResized = false;
 | 
			
		||||
 | 
			
		||||
        private const double _diff = 64;
 | 
			
		||||
 | 
			
		||||
        private string Id
 | 
			
		||||
        {
 | 
			
		||||
            get
 | 
			
		||||
            {
 | 
			
		||||
                _id ??= Guid.NewGuid().ToString();
 | 
			
		||||
 | 
			
		||||
                return _id;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Inject]
 | 
			
		||||
        public IJSRuntime JSRuntime { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [Inject]
 | 
			
		||||
        public IPopoverService PopoverService { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// If true, compact vertical padding will be applied to all menu items.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public bool Dense { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Set to true if you want to prevent page from scrolling when the menu is open
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public bool LockScroll { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// If true, the list menu will be same width as the parent.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public DropdownWidth RelativeWidth { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Sets the max height the menu can have when open.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public int? MaxHeight { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Set the anchor origin point to determine where the popover will open from.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public Origin AnchorOrigin { get; set; } = Origin.TopLeft;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Sets the transform origin point for the popover.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupAppearance)]
 | 
			
		||||
        public Origin TransformOrigin { get; set; } = Origin.TopLeft;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// If true, menu will be disabled.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.Behavior)]
 | 
			
		||||
        public bool Disabled { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.Appearance)]
 | 
			
		||||
        public bool Ripple { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Determines whether the component has a drop-shadow. Default is true
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.Appearance)]
 | 
			
		||||
        public bool DropShadow { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Add menu items here
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupBehavior)]
 | 
			
		||||
        public RenderFragment? ChildContent { get; set; }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Fired when the menu <see cref="Open"/> property changes.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        [Category(CategoryTypes.Menu.PopupBehavior)]
 | 
			
		||||
        public EventCallback<bool> OpenChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public int AdjustmentX { get; set; }
 | 
			
		||||
 | 
			
		||||
        [Parameter]
 | 
			
		||||
        public int AdjustmentY { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected MudMenu? FakeMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected void FakeOpenChanged(bool value)
 | 
			
		||||
        {
 | 
			
		||||
            if (!value)
 | 
			
		||||
            {
 | 
			
		||||
                _open = false;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            StateHasChanged();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Opens the menu.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        /// <param name="args">
 | 
			
		||||
        /// The arguments of the calling mouse/pointer event.
 | 
			
		||||
        /// </param>
 | 
			
		||||
        public async Task OpenMenuAsync(EventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            if (Disabled)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // long press on iOS triggers selection, so clear it
 | 
			
		||||
            await JSRuntime.ClearSelection();
 | 
			
		||||
 | 
			
		||||
            if (args is not LongPressEventArgs)
 | 
			
		||||
            {
 | 
			
		||||
                _showChildren = true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _open = true;
 | 
			
		||||
            _isResized = false;
 | 
			
		||||
            StateHasChanged();
 | 
			
		||||
 | 
			
		||||
            var (x, y) = GetPositionFromArgs(args);
 | 
			
		||||
            _x = x;
 | 
			
		||||
            _y = y;
 | 
			
		||||
 | 
			
		||||
            SetPopoverStyle(x, y);
 | 
			
		||||
 | 
			
		||||
            StateHasChanged();
 | 
			
		||||
 | 
			
		||||
            await OpenChanged.InvokeAsync(_open);
 | 
			
		||||
 | 
			
		||||
            // long press on iOS triggers selection, so clear it
 | 
			
		||||
            await JSRuntime.ClearSelection();
 | 
			
		||||
 | 
			
		||||
            if (args is LongPressEventArgs)
 | 
			
		||||
            {
 | 
			
		||||
                await Task.Delay(1000);
 | 
			
		||||
                _showChildren = true;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Closes the menu.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public Task CloseMenuAsync()
 | 
			
		||||
        {
 | 
			
		||||
            _open = false;
 | 
			
		||||
            _popoverStyle = null;
 | 
			
		||||
            StateHasChanged();
 | 
			
		||||
 | 
			
		||||
            return OpenChanged.InvokeAsync(_open);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void SetPopoverStyle(double x, double y)
 | 
			
		||||
        {
 | 
			
		||||
            _popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};";
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        /// <summary>
 | 
			
		||||
        /// Toggle the visibility of the menu.
 | 
			
		||||
        /// </summary>
 | 
			
		||||
        public async Task ToggleMenuAsync(EventArgs args)
 | 
			
		||||
        {
 | 
			
		||||
            if (Disabled)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_open)
 | 
			
		||||
            {
 | 
			
		||||
                await CloseMenuAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await OpenMenuAsync(args);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
        {
 | 
			
		||||
            if (!_isResized)
 | 
			
		||||
            {
 | 
			
		||||
                //await DeterminePosition();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return Task.CompletedTask;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        //private async Task DeterminePosition()
 | 
			
		||||
        //{
 | 
			
		||||
        //    var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
 | 
			
		||||
        //    double? contextMenuHeight = null;
 | 
			
		||||
        //    double? contextMenuWidth = null;
 | 
			
		||||
 | 
			
		||||
        //    var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
 | 
			
		||||
 | 
			
		||||
        //    var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
 | 
			
		||||
        //    if (popoverSize.Height > 0)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        contextMenuHeight = popoverSize.Height;
 | 
			
		||||
        //        contextMenuWidth = popoverSize.Width;
 | 
			
		||||
        //    }
 | 
			
		||||
        //    else
 | 
			
		||||
        //    {
 | 
			
		||||
        //        return;
 | 
			
		||||
        //    }
 | 
			
		||||
 | 
			
		||||
        //    // the bottom position of the popover will be rendered off screen
 | 
			
		||||
        //    if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        // adjust the top of the context menu
 | 
			
		||||
        //        var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
 | 
			
		||||
        //        _y -= overshoot;
 | 
			
		||||
 | 
			
		||||
        //        if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
 | 
			
		||||
        //        {
 | 
			
		||||
        //            MaxHeight = (int)(mainContentSize.Height - _y + _diff);
 | 
			
		||||
        //        }
 | 
			
		||||
        //    }
 | 
			
		||||
 | 
			
		||||
        //    if (_x + contextMenuWidth.Value > mainContentSize.Width)
 | 
			
		||||
        //    {
 | 
			
		||||
        //        var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
 | 
			
		||||
        //        _x -= overshoot;
 | 
			
		||||
        //    }
 | 
			
		||||
 | 
			
		||||
        //    SetPopoverStyle(_x, _y);
 | 
			
		||||
        //    _isResized = true;
 | 
			
		||||
        //    await InvokeAsync(StateHasChanged);
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
 | 
			
		||||
        {
 | 
			
		||||
            double x, y;
 | 
			
		||||
            if (eventArgs is MouseEventArgs mouseEventArgs)
 | 
			
		||||
            {
 | 
			
		||||
                x = mouseEventArgs.ClientX;
 | 
			
		||||
                y = mouseEventArgs.ClientY;
 | 
			
		||||
            }
 | 
			
		||||
            else if (eventArgs is LongPressEventArgs longPressEventArgs)
 | 
			
		||||
            {
 | 
			
		||||
                x = longPressEventArgs.ClientX;
 | 
			
		||||
                y = longPressEventArgs.ClientY;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                throw new NotSupportedException("Invalid eventArgs type.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (x + AdjustmentX, y + AdjustmentY);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
<div class="@Classname">
 | 
			
		||||
    <div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
 | 
			
		||||
    <div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
 | 
			
		||||
        @if (!string.IsNullOrEmpty(Icon))
 | 
			
		||||
        {
 | 
			
		||||
            <MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
 | 
			
		||||
 
 | 
			
		||||
@@ -59,6 +59,7 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
            new CssBuilder("mud-nav-link")
 | 
			
		||||
                .AddClass($"mud-nav-link-disabled", Disabled)
 | 
			
		||||
                .AddClass("active", Active)
 | 
			
		||||
                .AddClass("unselectable", OnLongPress.HasDelegate || OnContextMenu.HasDelegate)
 | 
			
		||||
                .Build();
 | 
			
		||||
 | 
			
		||||
        protected string IconClassname =>
 | 
			
		||||
 
 | 
			
		||||
@@ -81,6 +81,8 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
 | 
			
		||||
        protected HashSet<string> SelectedColumns { get; set; } = [];
 | 
			
		||||
 | 
			
		||||
        private static readonly IReadOnlyList<ColumnDefinition<T>> EmptyColumns = Array.Empty<ColumnDefinition<T>>();
 | 
			
		||||
 | 
			
		||||
        private Dictionary<string, int?> _columnWidths = [];
 | 
			
		||||
 | 
			
		||||
        private Dictionary<string, int> _columnOrder = [];
 | 
			
		||||
@@ -89,8 +91,16 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
 | 
			
		||||
        private SortDirection _sortDirection;
 | 
			
		||||
 | 
			
		||||
        private DateTimeOffset? _suppressRowClickUntil;
 | 
			
		||||
 | 
			
		||||
        private readonly Dictionary<string, TdExtended> _tds = [];
 | 
			
		||||
 | 
			
		||||
        private IReadOnlyList<ColumnDefinition<T>> _visibleColumns = EmptyColumns;
 | 
			
		||||
 | 
			
		||||
        private bool _columnsDirty = true;
 | 
			
		||||
 | 
			
		||||
        private IEnumerable<ColumnDefinition<T>>? _lastColumnDefinitions;
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnInitializedAsync()
 | 
			
		||||
        {
 | 
			
		||||
            HashSet<string> selectedColumns;
 | 
			
		||||
@@ -109,6 +119,13 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
                SelectedColumns = selectedColumns;
 | 
			
		||||
                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                SelectedColumns = selectedColumns;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _lastColumnDefinitions = ColumnDefinitions;
 | 
			
		||||
            MarkColumnsDirty();
 | 
			
		||||
 | 
			
		||||
            string? sortColumn;
 | 
			
		||||
            SortDirection sortDirection;
 | 
			
		||||
@@ -137,11 +154,24 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
                await SortDirectionChanged.InvokeAsync(_sortDirection);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            MarkColumnsDirty();
 | 
			
		||||
 | 
			
		||||
            var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
 | 
			
		||||
            if (storedColumnsWidths is not null)
 | 
			
		||||
            {
 | 
			
		||||
                _columnWidths = storedColumnsWidths;
 | 
			
		||||
            }
 | 
			
		||||
            MarkColumnsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void OnParametersSet()
 | 
			
		||||
        {
 | 
			
		||||
            base.OnParametersSet();
 | 
			
		||||
            if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
 | 
			
		||||
            {
 | 
			
		||||
                _lastColumnDefinitions = ColumnDefinitions;
 | 
			
		||||
                MarkColumnsDirty();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IEnumerable<T>? GetOrderedItems()
 | 
			
		||||
@@ -165,39 +195,74 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
            return Items.OrderByDirection(_sortDirection, sortSelector);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected IEnumerable<ColumnDefinition<T>> GetColumns()
 | 
			
		||||
        protected IReadOnlyList<ColumnDefinition<T>> GetColumns()
 | 
			
		||||
        {
 | 
			
		||||
            var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
 | 
			
		||||
            if (_columnOrder.Count == 0)
 | 
			
		||||
            if (!_columnsDirty)
 | 
			
		||||
            {
 | 
			
		||||
                foreach (var column in filteredColumns)
 | 
			
		||||
                {
 | 
			
		||||
                    if (_columnWidths.TryGetValue(column.Id, out var value))
 | 
			
		||||
                    {
 | 
			
		||||
                        column.Width = value;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    yield return column;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                yield break;
 | 
			
		||||
                return _visibleColumns;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
 | 
			
		||||
            foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key))
 | 
			
		||||
            _visibleColumns = BuildVisibleColumns();
 | 
			
		||||
            _columnsDirty = false;
 | 
			
		||||
 | 
			
		||||
            return _visibleColumns;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private IReadOnlyList<ColumnDefinition<T>> BuildVisibleColumns()
 | 
			
		||||
        {
 | 
			
		||||
            var filteredColumns = ColumnDefinitions
 | 
			
		||||
                .Where(c => SelectedColumns.Contains(c.Id))
 | 
			
		||||
                .Where(ColumnFilter)
 | 
			
		||||
                .ToList();
 | 
			
		||||
 | 
			
		||||
            if (filteredColumns.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                if (!columnDictionary.TryGetValue(columnId, out var column))
 | 
			
		||||
                return EmptyColumns;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            List<ColumnDefinition<T>> orderedColumns;
 | 
			
		||||
            if (_columnOrder.Count == 0)
 | 
			
		||||
            {
 | 
			
		||||
                orderedColumns = filteredColumns;
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList();
 | 
			
		||||
                var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
 | 
			
		||||
                orderedColumns = new List<ColumnDefinition<T>>(filteredColumns.Count);
 | 
			
		||||
 | 
			
		||||
                foreach (var (columnId, _) in orderLookup)
 | 
			
		||||
                {
 | 
			
		||||
                    continue;
 | 
			
		||||
                    if (!columnDictionary.TryGetValue(columnId, out var column))
 | 
			
		||||
                    {
 | 
			
		||||
                        continue;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    orderedColumns.Add(column);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (orderedColumns.Count != filteredColumns.Count)
 | 
			
		||||
                {
 | 
			
		||||
                    var existingIds = new HashSet<string>(orderedColumns.Select(c => c.Id));
 | 
			
		||||
                    foreach (var column in filteredColumns)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (existingIds.Add(column.Id))
 | 
			
		||||
                        {
 | 
			
		||||
                            orderedColumns.Add(column);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            foreach (var column in orderedColumns)
 | 
			
		||||
            {
 | 
			
		||||
                if (_columnWidths.TryGetValue(column.Id, out var value))
 | 
			
		||||
                {
 | 
			
		||||
                    column.Width = value;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                yield return column;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return orderedColumns;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task SetSort(string columnId, SortDirection sortDirection)
 | 
			
		||||
@@ -223,6 +288,17 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
 | 
			
		||||
        protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
 | 
			
		||||
        {
 | 
			
		||||
            if (_suppressRowClickUntil is not null)
 | 
			
		||||
            {
 | 
			
		||||
                if (DateTimeOffset.UtcNow <= _suppressRowClickUntil.Value)
 | 
			
		||||
                {
 | 
			
		||||
                    _suppressRowClickUntil = null;
 | 
			
		||||
                    return;
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                _suppressRowClickUntil = null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (eventArgs.Item is null)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
@@ -298,6 +374,7 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
 | 
			
		||||
        protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
 | 
			
		||||
        {
 | 
			
		||||
            _suppressRowClickUntil = DateTimeOffset.UtcNow.AddMilliseconds(500);
 | 
			
		||||
            var data = _tds[columnId];
 | 
			
		||||
            return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
 | 
			
		||||
        }
 | 
			
		||||
@@ -316,18 +393,21 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
                SelectedColumns = result.SelectedColumns;
 | 
			
		||||
                await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
 | 
			
		||||
                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
			
		||||
                MarkColumnsDirty();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
 | 
			
		||||
            {
 | 
			
		||||
                _columnWidths = result.ColumnWidths;
 | 
			
		||||
                await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
 | 
			
		||||
                MarkColumnsDirty();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
 | 
			
		||||
            {
 | 
			
		||||
                _columnOrder = result.ColumnOrder;
 | 
			
		||||
                await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
 | 
			
		||||
                MarkColumnsDirty();
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -368,17 +448,34 @@ namespace Lantean.QBTMud.Components.UI
 | 
			
		||||
 | 
			
		||||
            if (column.Width.HasValue)
 | 
			
		||||
            {
 | 
			
		||||
                className = $"overflow-cell {className}";
 | 
			
		||||
                className = string.IsNullOrWhiteSpace(className)
 | 
			
		||||
                    ? "overflow-cell"
 | 
			
		||||
                    : $"overflow-cell {className}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (OnTableDataContextMenu.HasDelegate)
 | 
			
		||||
            {
 | 
			
		||||
                className = $"no-default-context-menu {className}";
 | 
			
		||||
                className = string.IsNullOrWhiteSpace(className)
 | 
			
		||||
                    ? "no-default-context-menu"
 | 
			
		||||
                    : $"no-default-context-menu {className}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (OnTableDataLongPress.HasDelegate)
 | 
			
		||||
            {
 | 
			
		||||
                className = string.IsNullOrWhiteSpace(className)
 | 
			
		||||
                    ? "unselectable"
 | 
			
		||||
                    : $"unselectable {className}";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return className;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void MarkColumnsDirty()
 | 
			
		||||
        {
 | 
			
		||||
            _columnsDirty = true;
 | 
			
		||||
            _visibleColumns = EmptyColumns;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private sealed record SortData
 | 
			
		||||
        {
 | 
			
		||||
            public SortData(string sortColumn, SortDirection sortDirection)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
@inherits MudTd
 | 
			
		||||
 | 
			
		||||
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
 | 
			
		||||
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
 | 
			
		||||
    @ChildContent
 | 
			
		||||
</td>
 | 
			
		||||
@@ -1,6 +1,10 @@
 | 
			
		||||
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="WebSeeds"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              Class="details-list" />
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="WebSeeds"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      Class="details-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -435,22 +435,5 @@ namespace Lantean.QBTMud.Helpers
 | 
			
		||||
 | 
			
		||||
            await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static async Task<QBitTorrentClient.Models.SearchPlugin?> ShowSearchPluginsDialog(this IDialogService dialogService)
 | 
			
		||||
        {
 | 
			
		||||
            var parameters = new DialogParameters
 | 
			
		||||
            {
 | 
			
		||||
                { nameof(SearchPluginsDialog.Hashes), "" },
 | 
			
		||||
            };
 | 
			
		||||
            
 | 
			
		||||
            var result = await dialogService.ShowAsync<SearchPluginsDialog>("Search Plugins", parameters, FormDialogOptions);
 | 
			
		||||
            var dialogResult = await result.Result;
 | 
			
		||||
            if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
 | 
			
		||||
            {
 | 
			
		||||
                return null;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (QBitTorrentClient.Models.SearchPlugin)dialogResult.Data;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
 | 
			
		||||
        {
 | 
			
		||||
            if (seconds is null)
 | 
			
		||||
            {
 | 
			
		||||
                return "";
 | 
			
		||||
                return string.Empty;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (seconds == 8640000)
 | 
			
		||||
            const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
 | 
			
		||||
            var value = seconds.Value;
 | 
			
		||||
 | 
			
		||||
            if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
 | 
			
		||||
            {
 | 
			
		||||
                return "∞";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (seconds < 60)
 | 
			
		||||
            if (value <= 0)
 | 
			
		||||
            {
 | 
			
		||||
                return "< 1m";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            TimeSpan time;
 | 
			
		||||
            try
 | 
			
		||||
            var time = TimeSpan.FromSeconds(value);
 | 
			
		||||
            if (time.TotalMinutes < 1)
 | 
			
		||||
            {
 | 
			
		||||
                time = TimeSpan.FromSeconds(seconds.Value);
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                return "∞";
 | 
			
		||||
                return "< 1m";
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var sb = new StringBuilder();
 | 
			
		||||
            if (prefix is not null)
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										40
									
								
								Lantean.QBTMud/Helpers/EventArgsExtensions.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										40
									
								
								Lantean.QBTMud/Helpers/EventArgsExtensions.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,40 @@
 | 
			
		||||
using Microsoft.AspNetCore.Components.Web;
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Helpers
 | 
			
		||||
{
 | 
			
		||||
    public static class EventArgsExtensions
 | 
			
		||||
    {
 | 
			
		||||
        public static EventArgs NormalizeForContextMenu(this EventArgs eventArgs)
 | 
			
		||||
        {
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(eventArgs);
 | 
			
		||||
 | 
			
		||||
            if (eventArgs is LongPressEventArgs longPressEventArgs)
 | 
			
		||||
            {
 | 
			
		||||
                return longPressEventArgs.ToMouseEventArgs();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return eventArgs;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static MouseEventArgs ToMouseEventArgs(this LongPressEventArgs longPressEventArgs)
 | 
			
		||||
        {
 | 
			
		||||
            ArgumentNullException.ThrowIfNull(longPressEventArgs);
 | 
			
		||||
 | 
			
		||||
            return new MouseEventArgs
 | 
			
		||||
            {
 | 
			
		||||
                Button = 2,
 | 
			
		||||
                Buttons = 2,
 | 
			
		||||
                ClientX = longPressEventArgs.ClientX,
 | 
			
		||||
                ClientY = longPressEventArgs.ClientY,
 | 
			
		||||
                OffsetX = longPressEventArgs.OffsetX,
 | 
			
		||||
                OffsetY = longPressEventArgs.OffsetY,
 | 
			
		||||
                PageX = longPressEventArgs.PageX,
 | 
			
		||||
                PageY = longPressEventArgs.PageY,
 | 
			
		||||
                ScreenX = longPressEventArgs.ScreenX,
 | 
			
		||||
                ScreenY = longPressEventArgs.ScreenY,
 | 
			
		||||
                Type = longPressEventArgs.Type ?? "contextmenu",
 | 
			
		||||
                Detail = -1,
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -119,34 +119,35 @@ namespace Lantean.QBTMud.Helpers
 | 
			
		||||
            switch (category)
 | 
			
		||||
            {
 | 
			
		||||
                case CATEGORY_ALL:
 | 
			
		||||
                    break;
 | 
			
		||||
                    return true;
 | 
			
		||||
 | 
			
		||||
                case CATEGORY_UNCATEGORIZED:
 | 
			
		||||
                    if (!string.IsNullOrEmpty(torrent.Category))
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                    return true;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
                    if (string.IsNullOrEmpty(torrent.Category))
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    if (!useSubcategories)
 | 
			
		||||
                    {
 | 
			
		||||
                        if (torrent.Category != category)
 | 
			
		||||
                        {
 | 
			
		||||
                            return false;
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            if (!torrent.Category.StartsWith(category))
 | 
			
		||||
                            {
 | 
			
		||||
                                return false;
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                        return string.Equals(torrent.Category, category, StringComparison.Ordinal);
 | 
			
		||||
                    }
 | 
			
		||||
                    break;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return true;
 | 
			
		||||
                    if (string.Equals(torrent.Category, category, StringComparison.Ordinal))
 | 
			
		||||
                    {
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    var prefix = string.Concat(category, "/");
 | 
			
		||||
                    return torrent.Category.StartsWith(prefix, StringComparison.Ordinal);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        public static bool FilterTag(Torrent torrent, string tag)
 | 
			
		||||
@@ -207,7 +208,7 @@ namespace Lantean.QBTMud.Helpers
 | 
			
		||||
                    break;
 | 
			
		||||
 | 
			
		||||
                case Status.Paused:
 | 
			
		||||
                    if (!state.Contains("paused") || !state.Contains("stopped"))
 | 
			
		||||
                    if (!state.Contains("paused") && !state.Contains("stopped"))
 | 
			
		||||
                    {
 | 
			
		||||
                        return false;
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Helpers
 | 
			
		||||
namespace Lantean.QBTMud.Helpers
 | 
			
		||||
{
 | 
			
		||||
    internal static class VersionHelper
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
@inherits LayoutComponentBase
 | 
			
		||||
@layout LoggedInLayout
 | 
			
		||||
 | 
			
		||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
			
		||||
    <TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
 | 
			
		||||
</MudDrawer>
 | 
			
		||||
<MudMainContent>
 | 
			
		||||
    @Body
 | 
			
		||||
</MudMainContent>
 | 
			
		||||
<div class="app-shell__body">
 | 
			
		||||
    <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
 | 
			
		||||
        <TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
 | 
			
		||||
    </MudDrawer>
 | 
			
		||||
    <MudMainContent Class="app-shell__main">
 | 
			
		||||
        @Body
 | 
			
		||||
    </MudMainContent>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
@inherits LayoutComponentBase
 | 
			
		||||
@layout LoggedInLayout
 | 
			
		||||
 | 
			
		||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
			
		||||
    <FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged"  />
 | 
			
		||||
</MudDrawer>
 | 
			
		||||
<MudMainContent>
 | 
			
		||||
    <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
			
		||||
    @Body
 | 
			
		||||
    </CascadingValue>
 | 
			
		||||
</MudMainContent>
 | 
			
		||||
<div class="app-shell__body">
 | 
			
		||||
    <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
 | 
			
		||||
        <FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
 | 
			
		||||
    </MudDrawer>
 | 
			
		||||
    <MudMainContent Class="app-shell__main">
 | 
			
		||||
        <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
			
		||||
            @Body
 | 
			
		||||
        </CascadingValue>
 | 
			
		||||
    </MudMainContent>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -10,20 +10,53 @@
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
<CascadingValue Value="Torrents">
 | 
			
		||||
    <CascadingValue Value="MainData">
 | 
			
		||||
        <CascadingValue Value="Preferences">
 | 
			
		||||
            <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">
 | 
			
		||||
                                                    <CascadingValue Value="Version" Name="Version">
 | 
			
		||||
                                                        @Body
 | 
			
		||||
    <CascadingValue Value="_torrentsVersion" Name="TorrentsVersion">
 | 
			
		||||
        <CascadingValue Value="MainData">
 | 
			
		||||
            <CascadingValue Value="Preferences">
 | 
			
		||||
                <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">
 | 
			
		||||
                                                        <CascadingValue Value="Version" Name="Version">
 | 
			
		||||
                                                            <div class="app-shell">
 | 
			
		||||
                                                                @Body
 | 
			
		||||
                                                                <MudAppBar Bottom="true" Elevation="0" Dense="true" Class="app-shell__status-bar">
 | 
			
		||||
                                                                    @if (MainData?.LostConnection == true)
 | 
			
		||||
                                                                    {
 | 
			
		||||
                                                                        <MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
 | 
			
		||||
                                                                    }
 | 
			
		||||
                                                                    <MudSpacer />
 | 
			
		||||
                                                                    <MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
 | 
			
		||||
                                                                    <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
			
		||||
                                                                    <MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
 | 
			
		||||
                                                                    <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
			
		||||
                                                                    @{
 | 
			
		||||
                                                                        var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
 | 
			
		||||
                                                                    }
 | 
			
		||||
                                                                    <MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="@MainData?.ServerState.ConnectionStatus" />
 | 
			
		||||
                                                                    <MudDivider Vertical="true" Class="" />
 | 
			
		||||
                                                                    <MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
 | 
			
		||||
                                                                    <MudDivider Vertical="true" Class="" />
 | 
			
		||||
                                                                    <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
 | 
			
		||||
                                                                    <MudText Class="mr-1 mb-1">
 | 
			
		||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
 | 
			
		||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
 | 
			
		||||
                                                                    </MudText>
 | 
			
		||||
                                                                    <MudDivider Vertical="true" />
 | 
			
		||||
                                                                    <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Info" />
 | 
			
		||||
                                                                    <MudText Class="mr-1 mb-1">
 | 
			
		||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
 | 
			
		||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
 | 
			
		||||
                                                                    </MudText>
 | 
			
		||||
                                                                </MudAppBar>
 | 
			
		||||
                                                            </div>
 | 
			
		||||
                                                        </CascadingValue>
 | 
			
		||||
                                                    </CascadingValue>
 | 
			
		||||
                                                </CascadingValue>
 | 
			
		||||
                                            </CascadingValue>
 | 
			
		||||
@@ -36,34 +69,5 @@
 | 
			
		||||
                </CascadingValue>
 | 
			
		||||
            </CascadingValue>
 | 
			
		||||
        </CascadingValue>
 | 
			
		||||
        <MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
 | 
			
		||||
            @if (MainData?.LostConnection == true)
 | 
			
		||||
            {
 | 
			
		||||
                <MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
 | 
			
		||||
            }
 | 
			
		||||
            <MudSpacer />
 | 
			
		||||
            <MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
			
		||||
            <MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
			
		||||
            @{
 | 
			
		||||
                var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
 | 
			
		||||
            }
 | 
			
		||||
            <MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
 | 
			
		||||
            <MudDivider Vertical="true" Class="" />
 | 
			
		||||
            <MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
 | 
			
		||||
            <MudDivider Vertical="true" Class="" />
 | 
			
		||||
            <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
 | 
			
		||||
            <MudText Class="mr-1 mb-1">
 | 
			
		||||
                @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
 | 
			
		||||
                @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
 | 
			
		||||
            </MudText>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Info" />
 | 
			
		||||
            <MudText Class="mr-1 mb-1">
 | 
			
		||||
                @DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
 | 
			
		||||
                @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
 | 
			
		||||
            </MudText>
 | 
			
		||||
        </MudAppBar>
 | 
			
		||||
    </CascadingValue>
 | 
			
		||||
</CascadingValue>
 | 
			
		||||
@@ -52,22 +52,36 @@ namespace Lantean.QBTMud.Layout
 | 
			
		||||
 | 
			
		||||
        protected string? SearchText { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected IEnumerable<Torrent> Torrents => GetTorrents();
 | 
			
		||||
        protected IReadOnlyList<Torrent> Torrents => GetTorrents();
 | 
			
		||||
 | 
			
		||||
        protected bool IsAuthenticated { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected bool LostConnection { get; set; }
 | 
			
		||||
 | 
			
		||||
        private List<Torrent> GetTorrents()
 | 
			
		||||
        private IReadOnlyList<Torrent> _visibleTorrents = Array.Empty<Torrent>();
 | 
			
		||||
 | 
			
		||||
        private bool _torrentsDirty = true;
 | 
			
		||||
        private int _torrentsVersion;
 | 
			
		||||
 | 
			
		||||
        private IReadOnlyList<Torrent> GetTorrents()
 | 
			
		||||
        {
 | 
			
		||||
            if (!_torrentsDirty)
 | 
			
		||||
            {
 | 
			
		||||
                return _visibleTorrents;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (MainData is null)
 | 
			
		||||
            {
 | 
			
		||||
                return [];
 | 
			
		||||
                _visibleTorrents = Array.Empty<Torrent>();
 | 
			
		||||
                _torrentsDirty = false;
 | 
			
		||||
                return _visibleTorrents;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
 | 
			
		||||
            _visibleTorrents = MainData.Torrents.Values.Filter(filterState).ToList();
 | 
			
		||||
            _torrentsDirty = false;
 | 
			
		||||
 | 
			
		||||
            return MainData.Torrents.Values.Filter(filterState).ToList();
 | 
			
		||||
            return _visibleTorrents;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnInitializedAsync()
 | 
			
		||||
@@ -84,6 +98,7 @@ namespace Lantean.QBTMud.Layout
 | 
			
		||||
            Version = await ApiClient.GetApplicationVersion();
 | 
			
		||||
            var data = await ApiClient.GetMainData(_requestId);
 | 
			
		||||
            MainData = DataManager.CreateMainData(data, Version);
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
 | 
			
		||||
            _requestId = data.ResponseId;
 | 
			
		||||
            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
			
		||||
@@ -126,32 +141,51 @@ namespace Lantean.QBTMud.Layout
 | 
			
		||||
                            return;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        var shouldRender = false;
 | 
			
		||||
 | 
			
		||||
                        if (MainData is null || data.FullUpdate)
 | 
			
		||||
                        {
 | 
			
		||||
                            MainData = DataManager.CreateMainData(data, Version);
 | 
			
		||||
                            MarkTorrentsDirty();
 | 
			
		||||
                            shouldRender = true;
 | 
			
		||||
                        }
 | 
			
		||||
                        else
 | 
			
		||||
                        {
 | 
			
		||||
                            DataManager.MergeMainData(data, MainData);
 | 
			
		||||
                            var dataChanged = DataManager.MergeMainData(data, MainData, out var filterChanged);
 | 
			
		||||
                            if (filterChanged)
 | 
			
		||||
                            {
 | 
			
		||||
                                MarkTorrentsDirty();
 | 
			
		||||
                            }
 | 
			
		||||
                            else if (dataChanged)
 | 
			
		||||
                            {
 | 
			
		||||
                                IncrementTorrentsVersion();
 | 
			
		||||
                            }
 | 
			
		||||
                            shouldRender = dataChanged;
 | 
			
		||||
                        }
 | 
			
		||||
 | 
			
		||||
                        _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
			
		||||
                        if (MainData is not null)
 | 
			
		||||
                        {
 | 
			
		||||
                            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
			
		||||
                        }
 | 
			
		||||
                        _requestId = data.ResponseId;
 | 
			
		||||
                        await InvokeAsync(StateHasChanged);
 | 
			
		||||
                        if (shouldRender)
 | 
			
		||||
                        {
 | 
			
		||||
                            await InvokeAsync(StateHasChanged);
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
 | 
			
		||||
        protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, OnCategoryChanged);
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
 | 
			
		||||
        protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, OnStatusChanged);
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
 | 
			
		||||
        protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, OnTagChanged);
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
 | 
			
		||||
        protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, OnTrackerChanged);
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
 | 
			
		||||
        protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, OnSearchTermChanged);
 | 
			
		||||
 | 
			
		||||
        protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
 | 
			
		||||
 | 
			
		||||
@@ -159,12 +193,81 @@ namespace Lantean.QBTMud.Layout
 | 
			
		||||
 | 
			
		||||
        protected static (string, Color) GetConnectionIcon(string? status)
 | 
			
		||||
        {
 | 
			
		||||
            if (status is null)
 | 
			
		||||
            return status switch
 | 
			
		||||
            {
 | 
			
		||||
                return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
 | 
			
		||||
                "firewalled" => (Icons.Material.Outlined.SignalWifiStatusbarConnectedNoInternet4, Color.Warning),
 | 
			
		||||
                "connected" => (Icons.Material.Outlined.SignalWifi4Bar, Color.Success),
 | 
			
		||||
                _ => (Icons.Material.Outlined.SignalWifiOff, Color.Error),
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnCategoryChanged(string category)
 | 
			
		||||
        {
 | 
			
		||||
            if (Category == category)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
 | 
			
		||||
            Category = category;
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnStatusChanged(Status status)
 | 
			
		||||
        {
 | 
			
		||||
            if (Status == status)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Status = status;
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnTagChanged(string tag)
 | 
			
		||||
        {
 | 
			
		||||
            if (Tag == tag)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Tag = tag;
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnTrackerChanged(string tracker)
 | 
			
		||||
        {
 | 
			
		||||
            if (Tracker == tracker)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            Tracker = tracker;
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void OnSearchTermChanged(string term)
 | 
			
		||||
        {
 | 
			
		||||
            if (SearchText == term)
 | 
			
		||||
            {
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            SearchText = term;
 | 
			
		||||
            MarkTorrentsDirty();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void MarkTorrentsDirty()
 | 
			
		||||
        {
 | 
			
		||||
            _torrentsDirty = true;
 | 
			
		||||
            IncrementTorrentsVersion();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private void IncrementTorrentsVersion()
 | 
			
		||||
        {
 | 
			
		||||
            unchecked
 | 
			
		||||
            {
 | 
			
		||||
                _torrentsVersion++;
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected virtual void Dispose(bool disposing)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,13 @@
 | 
			
		||||
@inherits LayoutComponentBase
 | 
			
		||||
@layout LoggedInLayout
 | 
			
		||||
 | 
			
		||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
			
		||||
    <MudNavMenu>
 | 
			
		||||
        <ApplicationActions IsMenu="false" Preferences="Preferences" />
 | 
			
		||||
    </MudNavMenu>
 | 
			
		||||
</MudDrawer>
 | 
			
		||||
<MudMainContent>
 | 
			
		||||
    @Body
 | 
			
		||||
</MudMainContent>
 | 
			
		||||
<div class="app-shell__body">
 | 
			
		||||
    <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
 | 
			
		||||
        <MudNavMenu>
 | 
			
		||||
            <ApplicationActions IsMenu="false" Preferences="Preferences" />
 | 
			
		||||
        </MudNavMenu>
 | 
			
		||||
    </MudDrawer>
 | 
			
		||||
    <MudMainContent Class="app-shell__main">
 | 
			
		||||
        @Body
 | 
			
		||||
    </MudMainContent>
 | 
			
		||||
</div>
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,5 @@
 | 
			
		||||
        StalledDownloading,
 | 
			
		||||
        Checking,
 | 
			
		||||
        Errored,
 | 
			
		||||
        
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
using Lantean.QBitTorrentClient.Models;
 | 
			
		||||
 | 
			
		||||
namespace Lantean.QBTMud.Models
 | 
			
		||||
namespace Lantean.QBTMud.Models
 | 
			
		||||
{
 | 
			
		||||
    public record TorrentOptions
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,18 +1,22 @@
 | 
			
		||||
@page "/about"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudText Class="px-5 no-wrap">About</MudText>
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudText Class="px-5 no-wrap">About</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
 | 
			
		||||
    <MudTabPanel Text="About">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudTabs Elevation="2" ApplyEffectsToContainer="true">
 | 
			
		||||
            <MudTabPanel Text="About">
 | 
			
		||||
                <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 content-panel__container options-tab-contents">
 | 
			
		||||
            <MudGrid Class="mt-0 mb-4">
 | 
			
		||||
                <MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
 | 
			
		||||
                    <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
 | 
			
		||||
@@ -60,7 +64,7 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Authors">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
 | 
			
		||||
            <MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
 | 
			
		||||
 | 
			
		||||
            <MudGrid Class="mt-0 mb-4">
 | 
			
		||||
@@ -108,7 +112,7 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Special Thanks">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">I would first like to thank sourceforge.net for hosting qBittorrent project and for their support.</MudText>
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">I am pleased that people from all over the world are contributing to qBittorrent: Ishan Arora (India), Arnaud Demaizière (France) and Stephanos Antaris (Greece). Their help is greatly appreciated</MudText>
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">I also want to thank Στέφανος Αντάρης (santaris@csd.auth.gr) and Mirco Chinelli (infinity89@fastwebmail.it) for working on Mac OS X packaging.</MudText>
 | 
			
		||||
@@ -118,7 +122,7 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Translators">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">
 | 
			
		||||
                I would like to thank the people who volunteered to Circle qBittorrent.<br>
 | 
			
		||||
                Most of them Circled via <MudLink Target="https://www.transifex.com/sledgehammer999/qbittorrent/" Href="https://www.transifex.com/sledgehammer999/qbittorrent/">Transifex</MudLink> and some of them are mentioned below:<br>
 | 
			
		||||
@@ -168,7 +172,7 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Licence">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">
 | 
			
		||||
                The qBittorrent source code is licensed under the GNU General Public License, version 2 or (at your option) any later version (GPLv2+).
 | 
			
		||||
                However, this binary distribution is licensed under GNU General Public License, version 3 or (at your option) any later version (GPLv3+),
 | 
			
		||||
@@ -1061,7 +1065,7 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Software Used">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3 options-tab-contents">
 | 
			
		||||
            <MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
 | 
			
		||||
 | 
			
		||||
            <MudGrid Class="mt-1 mb-4">
 | 
			
		||||
@@ -1105,3 +1109,5 @@
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
</MudTabs>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,36 +1,41 @@
 | 
			
		||||
@page "/blocks"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
            <MudCardContent>
 | 
			
		||||
                <EditForm Model="Model" OnSubmit="Submit">
 | 
			
		||||
                    <MudGrid>
 | 
			
		||||
                        <MudItem md="10">
 | 
			
		||||
                            <MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem md="2">
 | 
			
		||||
                            <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                    </MudGrid>
 | 
			
		||||
                </EditForm>
 | 
			
		||||
            </MudCardContent>
 | 
			
		||||
        </MudCard>
 | 
			
		||||
 | 
			
		||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
    <MudCardContent>
 | 
			
		||||
        <EditForm Model="Model" OnSubmit="Submit">
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem md="10">
 | 
			
		||||
                    <MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem md="2">
 | 
			
		||||
                    <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
        </EditForm>
 | 
			
		||||
    </MudCardContent>
 | 
			
		||||
</MudCard>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="Lantean.QBitTorrentClient.Models.PeerLog"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Results"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              RowClassFunc="RowClass"
 | 
			
		||||
              Class="search-list" />
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="Lantean.QBitTorrentClient.Models.PeerLog"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Results"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      RowClassFunc="RowClass"
 | 
			
		||||
                      Class="search-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,24 +1,30 @@
 | 
			
		||||
@page "/categories"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudText Class="px-5 no-wrap">Categories</MudText>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudText Class="px-5 no-wrap">Categories</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="Category"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Results"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              Class="details-list" />
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="Category"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Results"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      Class="details-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private RenderFragment<RowContext<Category>> ActionsColumn
 | 
			
		||||
 
 | 
			
		||||
@@ -1,41 +1,45 @@
 | 
			
		||||
@page "/details/{hash}"
 | 
			
		||||
@layout DetailsLayout
 | 
			
		||||
 | 
			
		||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    @if (Hash is not null)
 | 
			
		||||
    {
 | 
			
		||||
        <TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudText Class="pl-5 no-wrap">@Name</MudText>
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
</div>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            @if (Hash is not null)
 | 
			
		||||
            {
 | 
			
		||||
                <TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudText Class="pl-5 no-wrap">@Name</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
@if (ShowTabs)
 | 
			
		||||
{
 | 
			
		||||
    <CascadingValue Value="RefreshInterval" Name="RefreshInterval">
 | 
			
		||||
        <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
 | 
			
		||||
            <MudTabPanel Text="General">
 | 
			
		||||
                <GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Trackers">
 | 
			
		||||
                <TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Peers">
 | 
			
		||||
                <PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="HTTP Sources">
 | 
			
		||||
                <WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Content">
 | 
			
		||||
                <FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
        </MudTabs>
 | 
			
		||||
    </CascadingValue>
 | 
			
		||||
}
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        @if (ShowTabs)
 | 
			
		||||
        {
 | 
			
		||||
            <CascadingValue Value="RefreshInterval" Name="RefreshInterval">
 | 
			
		||||
                <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
 | 
			
		||||
                    <MudTabPanel Text="General">
 | 
			
		||||
                        <GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
 | 
			
		||||
                    </MudTabPanel>
 | 
			
		||||
                    <MudTabPanel Text="Trackers">
 | 
			
		||||
                        <TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
 | 
			
		||||
                    </MudTabPanel>
 | 
			
		||||
                    <MudTabPanel Text="Peers">
 | 
			
		||||
                        <PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
 | 
			
		||||
                    </MudTabPanel>
 | 
			
		||||
                    <MudTabPanel Text="HTTP Sources">
 | 
			
		||||
                        <WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
 | 
			
		||||
                    </MudTabPanel>
 | 
			
		||||
                    <MudTabPanel Text="Content">
 | 
			
		||||
                        <FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
 | 
			
		||||
                    </MudTabPanel>
 | 
			
		||||
                </MudTabs>
 | 
			
		||||
            </CascadingValue>
 | 
			
		||||
        }
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,44 +1,49 @@
 | 
			
		||||
@page "/log"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudText Class="pl-5 no-wrap">Execution Log</MudText>
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudText Class="pl-5 no-wrap">Execution Log</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
            <MudCardContent>
 | 
			
		||||
                <EditForm Model="Model" OnSubmit="Submit">
 | 
			
		||||
                    <MudGrid>
 | 
			
		||||
                        <MudItem md="7">
 | 
			
		||||
                            <MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem md="3">
 | 
			
		||||
                            <MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
 | 
			
		||||
                                <MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
 | 
			
		||||
                                <MudSelectItem Value="@("Info")">Info</MudSelectItem>
 | 
			
		||||
                                <MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
 | 
			
		||||
                                <MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
 | 
			
		||||
                            </MudSelect>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem md="2">
 | 
			
		||||
                            <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                    </MudGrid>
 | 
			
		||||
                </EditForm>
 | 
			
		||||
            </MudCardContent>
 | 
			
		||||
        </MudCard>
 | 
			
		||||
 | 
			
		||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
    <MudCardContent>
 | 
			
		||||
        <EditForm Model="Model" OnSubmit="Submit">
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem md="7">
 | 
			
		||||
                    <MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem md="3">
 | 
			
		||||
                    <MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
 | 
			
		||||
                        <MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
 | 
			
		||||
                        <MudSelectItem Value="@("Info")">Info</MudSelectItem>
 | 
			
		||||
                        <MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
 | 
			
		||||
                        <MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
 | 
			
		||||
                    </MudSelect>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem md="2">
 | 
			
		||||
                    <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
        </EditForm>
 | 
			
		||||
    </MudCardContent>
 | 
			
		||||
</MudCard>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="Lantean.QBitTorrentClient.Models.Log"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Results"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              RowClassFunc="RowClass"
 | 
			
		||||
              Class="search-list" />
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="Lantean.QBitTorrentClient.Models.Log"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Results"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      RowClassFunc="RowClass"
 | 
			
		||||
                      Class="search-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -3,41 +3,63 @@
 | 
			
		||||
 | 
			
		||||
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudText Class="px-5 no-wrap">Settings</MudText>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudText Class="px-5 no-wrap">Settings</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
 | 
			
		||||
    <MudTabPanel Text="Behaviour">
 | 
			
		||||
        <BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Downloads">
 | 
			
		||||
        <DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Connection">
 | 
			
		||||
        <ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Speed">
 | 
			
		||||
        <SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="BitTorrent">
 | 
			
		||||
        <BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="RSS">
 | 
			
		||||
        <RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Web UI">
 | 
			
		||||
        <WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
    <MudTabPanel Text="Advanced">
 | 
			
		||||
        <AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
    </MudTabPanel>
 | 
			
		||||
</MudTabs>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
 | 
			
		||||
            <MudTabPanel Text="Behaviour">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Downloads">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Connection">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Speed">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="BitTorrent">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="RSS">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Web UI">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
            <MudTabPanel Text="Advanced">
 | 
			
		||||
                <div class="options-tab-contents">
 | 
			
		||||
                    <AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
			
		||||
                </div>
 | 
			
		||||
            </MudTabPanel>
 | 
			
		||||
        </MudTabs>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,73 +1,79 @@
 | 
			
		||||
@page "/rss"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudText Class="px-5 no-wrap">RSS</MudText>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" Disabled="@(SelectedFeed is null)" title="Mark items read" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
 | 
			
		||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
 | 
			
		||||
    <MudGrid Class="rss-contents">
 | 
			
		||||
        <MudItem xs="4" Style="height: 100%">
 | 
			
		||||
            <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
 | 
			
		||||
                <MudListItem Icon="@Icons.Material.Filled.MarkEmailUnread" Text="@($"Unread ({UnreadCount})")" Value="@("unread")" />
 | 
			
		||||
                @foreach (var (key, feed) in Feeds)
 | 
			
		||||
                {
 | 
			
		||||
                    <MudListItem Icon="@(feed.IsLoading ? Icons.Material.Filled.Sync : Icons.Material.Filled.Wifi)" Class="@(feed.IsLoading ? "spin-animation" : "")" Text="@($"{feed.Title} ({feed.UnreadCount})")" Value="@key" />
 | 
			
		||||
                }
 | 
			
		||||
            </MudList>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="4" Style="height: 100%; overflow: auto">
 | 
			
		||||
            @if (Articles.Count > 0)
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged" Dense>
 | 
			
		||||
                    @foreach (var article in Articles)
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudText Class="px-5 no-wrap">RSS</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" Disabled="@(SelectedFeed is null)" title="Mark items read" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="content-panel__container">
 | 
			
		||||
            <MudGrid Class="rss-contents">
 | 
			
		||||
                <MudItem xs="4" Style="height: 100%">
 | 
			
		||||
                    <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
 | 
			
		||||
                        <MudListItem Icon="@Icons.Material.Filled.MarkEmailUnread" Text="@($"Unread ({UnreadCount})")" Value="@("unread")" />
 | 
			
		||||
                        @foreach (var (key, feed) in Feeds)
 | 
			
		||||
                        {
 | 
			
		||||
                            <MudListItem Icon="@(feed.IsLoading ? Icons.Material.Filled.Sync : Icons.Material.Filled.Wifi)" Class="@(feed.IsLoading ? "spin-animation" : "")" Text="@($"{feed.Title} ({feed.UnreadCount})")" Value="@key" />
 | 
			
		||||
                        }
 | 
			
		||||
                    </MudList>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="4" Style="height: 100%; overflow: auto">
 | 
			
		||||
                    @if (Articles.Count > 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudListItem Text="@article.Title" Value="article.Id" Icon="@Icons.Material.Filled.Check" IconColor="@(article.IsRead ? Color.Success : Color.Transparent)" />
 | 
			
		||||
                        <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged" Dense>
 | 
			
		||||
                            @foreach (var article in Articles)
 | 
			
		||||
                            {
 | 
			
		||||
                                <MudListItem Text="@article.Title" Value="article.Id" Icon="@Icons.Material.Filled.Check" IconColor="@(article.IsRead ? Color.Success : Color.Transparent)" />
 | 
			
		||||
                            }
 | 
			
		||||
                        </MudList>
 | 
			
		||||
                    }
 | 
			
		||||
                </MudList>
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
 | 
			
		||||
            }
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="4" Style="height: 100%">
 | 
			
		||||
            @if (Article is not null)
 | 
			
		||||
            {
 | 
			
		||||
                <MudCard>
 | 
			
		||||
                    <MudCardHeader>
 | 
			
		||||
                        <CardHeaderContent>
 | 
			
		||||
                            <MudText Typo="Typo.h6" Style="overflow-wrap: anywhere">@Article.Title</MudText>
 | 
			
		||||
                        </CardHeaderContent>
 | 
			
		||||
                        <CardHeaderActions>
 | 
			
		||||
                            <MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
 | 
			
		||||
                                <MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(Article.TorrentURL)" title="Download">Download</MudMenuItem>
 | 
			
		||||
                                <MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@Article.TorrentURL" Target="@Article.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
 | 
			
		||||
                            </MudMenu>
 | 
			
		||||
                        </CardHeaderActions>
 | 
			
		||||
                    </MudCardHeader>
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
 | 
			
		||||
                    }
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="4" Style="height: 100%">
 | 
			
		||||
                    @if (Article is not null)
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudCard>
 | 
			
		||||
                            <MudCardHeader>
 | 
			
		||||
                                <CardHeaderContent>
 | 
			
		||||
                                    <MudText Typo="Typo.h6" Style="overflow-wrap: anywhere">@Article.Title</MudText>
 | 
			
		||||
                                </CardHeaderContent>
 | 
			
		||||
                                <CardHeaderActions>
 | 
			
		||||
                                    <MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
 | 
			
		||||
                                        <MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(Article.TorrentURL)" title="Download">Download</MudMenuItem>
 | 
			
		||||
                                        <MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@Article.TorrentURL" Target="@Article.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
 | 
			
		||||
                                    </MudMenu>
 | 
			
		||||
                                </CardHeaderActions>
 | 
			
		||||
                            </MudCardHeader>
 | 
			
		||||
 | 
			
		||||
                    <MudCardContent>
 | 
			
		||||
                        <MudText Typo="Typo.subtitle2">@Article.Date</MudText>
 | 
			
		||||
                        <MudText Typo="Typo.body1">@Article.Description</MudText>
 | 
			
		||||
                    </MudCardContent>
 | 
			
		||||
                </MudCard>
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
 | 
			
		||||
            }
 | 
			
		||||
        </MudItem>
 | 
			
		||||
    </MudGrid>
 | 
			
		||||
</MudContainer>
 | 
			
		||||
                            <MudCardContent>
 | 
			
		||||
                                <MudText Typo="Typo.subtitle2">@Article.Date</MudText>
 | 
			
		||||
                                <MudText Typo="Typo.body1">@Article.Description</MudText>
 | 
			
		||||
                            </MudCardContent>
 | 
			
		||||
                        </MudCard>
 | 
			
		||||
                    }
 | 
			
		||||
                    else
 | 
			
		||||
                    {
 | 
			
		||||
                        <MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
 | 
			
		||||
                    }
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,64 +1,67 @@
 | 
			
		||||
@page "/search"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudText Class="pl-5 no-wrap">Search</MudText>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Settings" OnClick="ShowSearchPlugins" title="Search plugins" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudText Class="pl-5 no-wrap">Search</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
            <MudCardContent>
 | 
			
		||||
                <EditForm Model="Model" OnValidSubmit="DoSearch">
 | 
			
		||||
                    <MudGrid>
 | 
			
		||||
                        <MudItem xs="12" md="4">
 | 
			
		||||
                            <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem xs="12" md="3">
 | 
			
		||||
                            <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
 | 
			
		||||
                                @foreach (var (value, name) in Categories)
 | 
			
		||||
                                {
 | 
			
		||||
                                    <MudSelectItem Value="value">@name</MudSelectItem>
 | 
			
		||||
                                    if (value == "all")
 | 
			
		||||
                                    {
 | 
			
		||||
                                        <MudDivider />
 | 
			
		||||
                                    }
 | 
			
		||||
                                }
 | 
			
		||||
                            </MudSelect>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem xs="12" md="3">
 | 
			
		||||
                            <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
 | 
			
		||||
                                <MudSelectItem Value="@("all")">All</MudSelectItem>
 | 
			
		||||
                                @if (Plugins.Count > 0)
 | 
			
		||||
                                {
 | 
			
		||||
                                    <MudDivider />
 | 
			
		||||
 | 
			
		||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
			
		||||
    <MudCardContent>
 | 
			
		||||
        <EditForm Model="Model" OnValidSubmit="DoSearch">
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem xs="12" md="4">
 | 
			
		||||
                    <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12" md="3">
 | 
			
		||||
                    <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
 | 
			
		||||
                        @foreach (var (value, name) in Categories)
 | 
			
		||||
                        {
 | 
			
		||||
                            <MudSelectItem Value="value">@name</MudSelectItem>
 | 
			
		||||
                            if (value == "all")
 | 
			
		||||
                            {
 | 
			
		||||
                                <MudDivider />
 | 
			
		||||
                            }
 | 
			
		||||
                        }
 | 
			
		||||
                    </MudSelect>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12" md="3">
 | 
			
		||||
                    <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
 | 
			
		||||
                        <MudSelectItem Value="@("all")">All</MudSelectItem>
 | 
			
		||||
                        @if (Plugins.Count > 0)
 | 
			
		||||
                        {
 | 
			
		||||
                            <MudDivider />
 | 
			
		||||
                                }
 | 
			
		||||
                                @foreach (var (value, name) in Plugins)
 | 
			
		||||
                                {
 | 
			
		||||
                                    <MudSelectItem Value="value">@name</MudSelectItem>
 | 
			
		||||
                                }
 | 
			
		||||
                            </MudSelect>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                        <MudItem xs="12" md="2">
 | 
			
		||||
                            <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
 | 
			
		||||
                        </MudItem>
 | 
			
		||||
                    
 | 
			
		||||
                        }
 | 
			
		||||
                        @foreach (var (value, name) in Plugins)
 | 
			
		||||
                        {
 | 
			
		||||
                            <MudSelectItem Value="value">@name</MudSelectItem>
 | 
			
		||||
                        }
 | 
			
		||||
                    </MudSelect>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12" md="2">
 | 
			
		||||
                    <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                    </MudGrid>
 | 
			
		||||
                </EditForm>
 | 
			
		||||
            </MudCardContent>
 | 
			
		||||
        </MudCard>
 | 
			
		||||
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
        </EditForm>
 | 
			
		||||
    </MudCardContent>
 | 
			
		||||
</MudCard>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="Lantean.QBitTorrentClient.Models.SearchResult"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Results"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              Class="search-list" />
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="Lantean.QBitTorrentClient.Models.SearchResult"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Results"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      Class="search-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -150,11 +150,6 @@ namespace Lantean.QBTMud.Pages
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task ShowSearchPlugins()
 | 
			
		||||
        {
 | 
			
		||||
            await DialogService.ShowSearchPluginsDialog();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected IEnumerable<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> Columns => ColumnsDefinitions;
 | 
			
		||||
 | 
			
		||||
        public static List<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> ColumnsDefinitions { get; } =
 | 
			
		||||
 
 | 
			
		||||
@@ -1,62 +1,68 @@
 | 
			
		||||
@page "/statistics"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudText Class="pl-5 no-wrap">Statistics</MudText>
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudText Class="pl-5 no-wrap">Statistics</MudText>
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents">
 | 
			
		||||
    <MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
 | 
			
		||||
    <MudGrid>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="All-time uploaded">@DisplayHelpers.Size(ServerState?.AllTimeUploaded)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="All-time downloaded">@DisplayHelpers.Size(ServerState?.AllTimeDownloaded)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="All-time share ratio">@DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00")</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Session waste">@DisplayHelpers.Size(ServerState?.TotalWastedSession)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Connected peers">@DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
    </MudGrid>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents content-panel__container">
 | 
			
		||||
            <MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="All-time uploaded">@DisplayHelpers.Size(ServerState?.AllTimeUploaded)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="All-time downloaded">@DisplayHelpers.Size(ServerState?.AllTimeDownloaded)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="All-time share ratio">@DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00")</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Session waste">@DisplayHelpers.Size(ServerState?.TotalWastedSession)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Connected peers">@DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
 | 
			
		||||
    <MudText Typo="Typo.subtitle2" Class="pt-6">Cache statistics</MudText>
 | 
			
		||||
    <MudGrid>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Read cache hits">@DisplayHelpers.Percentage(ServerState?.ReadCacheHits)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Total buffer size">@DisplayHelpers.Size(ServerState?.TotalBuffersSize)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
    </MudGrid>
 | 
			
		||||
            <MudText Typo="Typo.subtitle2" Class="pt-6">Cache statistics</MudText>
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Read cache hits">@DisplayHelpers.Percentage(ServerState?.ReadCacheHits)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Total buffer size">@DisplayHelpers.Size(ServerState?.TotalBuffersSize)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
 | 
			
		||||
    <MudText Typo="Typo.subtitle2" Class="pt-6">Performance statistics</MudText>
 | 
			
		||||
    <MudGrid>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Write cache overload">@DisplayHelpers.Percentage(ServerState?.WriteCacheOverload)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Read cache overload">@DisplayHelpers.Percentage(ServerState?.ReadCacheOverload)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Queued I/O jobs">@DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Average time in queue">@DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms")</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
        <MudItem xs="12">
 | 
			
		||||
            <MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField>
 | 
			
		||||
        </MudItem>
 | 
			
		||||
    </MudGrid>
 | 
			
		||||
</MudContainer>
 | 
			
		||||
            <MudText Typo="Typo.subtitle2" Class="pt-6">Performance statistics</MudText>
 | 
			
		||||
            <MudGrid>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Write cache overload">@DisplayHelpers.Percentage(ServerState?.WriteCacheOverload)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Read cache overload">@DisplayHelpers.Percentage(ServerState?.ReadCacheOverload)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Queued I/O jobs">@DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Average time in queue">@DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms")</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
                <MudItem xs="12">
 | 
			
		||||
                    <MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField>
 | 
			
		||||
                </MudItem>
 | 
			
		||||
            </MudGrid>
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
@@ -1,24 +1,30 @@
 | 
			
		||||
@page "/tags"
 | 
			
		||||
@layout OtherLayout
 | 
			
		||||
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    @if (!DrawerOpen)
 | 
			
		||||
    {
 | 
			
		||||
        <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
        <MudDivider Vertical="true" />
 | 
			
		||||
    }
 | 
			
		||||
    <MudText Class="px-5 no-wrap">Tags</MudText>
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
 | 
			
		||||
</MudToolBar>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            @if (!DrawerOpen)
 | 
			
		||||
            {
 | 
			
		||||
                <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
 | 
			
		||||
                <MudDivider Vertical="true" />
 | 
			
		||||
            }
 | 
			
		||||
            <MudText Class="px-5 no-wrap">Tags</MudText>
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
 | 
			
		||||
        </MudToolBar>
 | 
			
		||||
    </div>
 | 
			
		||||
 | 
			
		||||
<DynamicTable @ref="Table"
 | 
			
		||||
              T="string"
 | 
			
		||||
              ColumnDefinitions="Columns"
 | 
			
		||||
              Items="Results"
 | 
			
		||||
              MultiSelection="false"
 | 
			
		||||
              SelectOnRowClick="false"
 | 
			
		||||
              Class="details-list" />
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <DynamicTable @ref="Table"
 | 
			
		||||
                      T="string"
 | 
			
		||||
                      ColumnDefinitions="Columns"
 | 
			
		||||
                      Items="Results"
 | 
			
		||||
                      MultiSelection="false"
 | 
			
		||||
                      SelectOnRowClick="false"
 | 
			
		||||
                      Class="details-list content-panel__table" />
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private RenderFragment<RowContext<string>> ActionsColumn
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +1,47 @@
 | 
			
		||||
@page "/"
 | 
			
		||||
@layout ListLayout
 | 
			
		||||
 | 
			
		||||
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
 | 
			
		||||
<MudMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
 | 
			
		||||
    <MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
 | 
			
		||||
    <MudDivider />
 | 
			
		||||
    <TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
			
		||||
</ContextMenu>
 | 
			
		||||
</MudMenu>
 | 
			
		||||
 | 
			
		||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
 | 
			
		||||
<MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
			
		||||
    <MudDivider Vertical="true" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrentToolbar" title="View torrent details" />
 | 
			
		||||
    <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
    <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>
 | 
			
		||||
<div class="content-panel">
 | 
			
		||||
    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
			
		||||
        <MudToolBar Gutters="false" Dense="true">
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
			
		||||
            <MudDivider Vertical="true" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrentToolbar" title="View torrent details" />
 | 
			
		||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
			
		||||
            <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>
 | 
			
		||||
    </div>
 | 
			
		||||
    <div class="content-panel__body">
 | 
			
		||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0 content-panel__container">
 | 
			
		||||
            <DynamicTable
 | 
			
		||||
                @ref="Table"
 | 
			
		||||
                T="Torrent" 
 | 
			
		||||
                Class="torrent-list content-panel__table"
 | 
			
		||||
                ColumnDefinitions="Columns" 
 | 
			
		||||
                Items="Torrents" 
 | 
			
		||||
                OnRowClick="RowClick" 
 | 
			
		||||
                MultiSelection="true"
 | 
			
		||||
                SelectOnRowClick="true"
 | 
			
		||||
                SelectedItemsChanged="SelectedItemsChanged"
 | 
			
		||||
                SortColumnChanged="SortColumnChangedHandler"
 | 
			
		||||
                SortDirectionChanged="SortDirectionChangedHandler"
 | 
			
		||||
                OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
                OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
            />
 | 
			
		||||
        </MudContainer>
 | 
			
		||||
    </div>
 | 
			
		||||
</div>
 | 
			
		||||
 | 
			
		||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
 | 
			
		||||
    <DynamicTable
 | 
			
		||||
        @ref="Table"
 | 
			
		||||
        T="Torrent" 
 | 
			
		||||
        Class="torrent-list"
 | 
			
		||||
        ColumnDefinitions="Columns" 
 | 
			
		||||
        Items="Torrents" 
 | 
			
		||||
        OnRowClick="RowClick" 
 | 
			
		||||
        MultiSelection="true"
 | 
			
		||||
        SelectOnRowClick="true"
 | 
			
		||||
        SelectedItemsChanged="SelectedItemsChanged"
 | 
			
		||||
        SortColumnChanged="SortColumnChangedHandler"
 | 
			
		||||
        SortDirectionChanged="SortDirectionChangedHandler"
 | 
			
		||||
        OnTableDataContextMenu="TableDataContextMenu"
 | 
			
		||||
        OnTableDataLongPress="TableDataLongPress"
 | 
			
		||||
    />
 | 
			
		||||
</MudContainer>
 | 
			
		||||
 | 
			
		||||
@code {
 | 
			
		||||
    private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
 | 
			
		||||
    {
 | 
			
		||||
 
 | 
			
		||||
@@ -35,11 +35,17 @@ namespace Lantean.QBTMud.Pages
 | 
			
		||||
        public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        public IEnumerable<Torrent>? Torrents { get; set; }
 | 
			
		||||
        public IReadOnlyList<Torrent>? Torrents { get; set; }
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter]
 | 
			
		||||
        public MainData MainData { get; set; } = default!;
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter(Name = "LostConnection")]
 | 
			
		||||
        public bool LostConnection { get; set; }
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter(Name = "TorrentsVersion")]
 | 
			
		||||
        public int TorrentsVersion { get; set; }
 | 
			
		||||
 | 
			
		||||
        [CascadingParameter(Name = "SearchTermChanged")]
 | 
			
		||||
        public EventCallback<string> SearchTermChanged { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -56,13 +62,23 @@ namespace Lantean.QBTMud.Pages
 | 
			
		||||
 | 
			
		||||
        protected HashSet<Torrent> SelectedItems { get; set; } = [];
 | 
			
		||||
 | 
			
		||||
        protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
 | 
			
		||||
        protected bool ToolbarButtonsEnabled => _toolbarButtonsEnabled;
 | 
			
		||||
 | 
			
		||||
        protected DynamicTable<Torrent>? Table { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected Torrent? ContextMenuItem { get; set; }
 | 
			
		||||
 | 
			
		||||
        protected ContextMenu? ContextMenu { get; set; }
 | 
			
		||||
        protected MudMenu? ContextMenu { get; set; }
 | 
			
		||||
 | 
			
		||||
        private object? _lastRenderedTorrents;
 | 
			
		||||
        private QBitTorrentClient.Models.Preferences? _lastPreferences;
 | 
			
		||||
        private bool _lastLostConnection;
 | 
			
		||||
        private bool _hasRendered;
 | 
			
		||||
        private int _lastSelectionCount;
 | 
			
		||||
        private int _lastTorrentsVersion = -1;
 | 
			
		||||
        private bool _pendingSelectionChange;
 | 
			
		||||
 | 
			
		||||
        private bool _toolbarButtonsEnabled;
 | 
			
		||||
 | 
			
		||||
        protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
			
		||||
        {
 | 
			
		||||
@@ -73,9 +89,81 @@ namespace Lantean.QBTMud.Pages
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override bool ShouldRender()
 | 
			
		||||
        {
 | 
			
		||||
            if (!_hasRendered)
 | 
			
		||||
            {
 | 
			
		||||
                _hasRendered = true;
 | 
			
		||||
                _lastRenderedTorrents = Torrents;
 | 
			
		||||
                _lastPreferences = Preferences;
 | 
			
		||||
                _lastLostConnection = LostConnection;
 | 
			
		||||
                _lastTorrentsVersion = TorrentsVersion;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_pendingSelectionChange)
 | 
			
		||||
            {
 | 
			
		||||
                _pendingSelectionChange = false;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_lastTorrentsVersion != TorrentsVersion)
 | 
			
		||||
            {
 | 
			
		||||
                _lastTorrentsVersion = TorrentsVersion;
 | 
			
		||||
                _lastRenderedTorrents = Torrents;
 | 
			
		||||
                _lastPreferences = Preferences;
 | 
			
		||||
                _lastLostConnection = LostConnection;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!ReferenceEquals(_lastRenderedTorrents, Torrents))
 | 
			
		||||
            {
 | 
			
		||||
                _lastRenderedTorrents = Torrents;
 | 
			
		||||
                _lastPreferences = Preferences;
 | 
			
		||||
                _lastLostConnection = LostConnection;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (!ReferenceEquals(_lastPreferences, Preferences))
 | 
			
		||||
            {
 | 
			
		||||
                _lastPreferences = Preferences;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_lastLostConnection != LostConnection)
 | 
			
		||||
            {
 | 
			
		||||
                _lastLostConnection = LostConnection;
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            if (_lastSelectionCount != SelectedItems.Count)
 | 
			
		||||
            {
 | 
			
		||||
                _lastSelectionCount = SelectedItems.Count;
 | 
			
		||||
                _toolbarButtonsEnabled = _lastSelectionCount > 0;
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            return false;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
 | 
			
		||||
        {
 | 
			
		||||
            SelectedItems = selectedItems;
 | 
			
		||||
            _toolbarButtonsEnabled = SelectedItems.Count > 0;
 | 
			
		||||
            _pendingSelectionChange = true;
 | 
			
		||||
            InvokeAsync(StateHasChanged);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
 | 
			
		||||
@@ -185,7 +273,9 @@ namespace Lantean.QBTMud.Pages
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.ToggleMenuAsync(eventArgs);
 | 
			
		||||
            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
			
		||||
 | 
			
		||||
            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
using Blazored.LocalStorage;
 | 
			
		||||
using Blazored.LocalStorage;
 | 
			
		||||
using Lantean.QBitTorrentClient;
 | 
			
		||||
using Lantean.QBTMud.Services;
 | 
			
		||||
using Microsoft.AspNetCore.Components.Web;
 | 
			
		||||
 
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Services
 | 
			
		||||
 | 
			
		||||
        Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
 | 
			
		||||
 | 
			
		||||
        void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList);
 | 
			
		||||
        bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
 | 
			
		||||
 | 
			
		||||
        PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
 | 
			
		||||
 | 
			
		||||
@@ -16,7 +16,7 @@ namespace Lantean.QBTMud.Services
 | 
			
		||||
 | 
			
		||||
        Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
 | 
			
		||||
 | 
			
		||||
        void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
 | 
			
		||||
        bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
 | 
			
		||||
 | 
			
		||||
        QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -65,15 +65,11 @@ code {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mud-appbar.mud-appbar-fixed-bottom {
 | 
			
		||||
    height: 35px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mud-main-content {
 | 
			
		||||
    padding-bottom: 35px;
 | 
			
		||||
    height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.mud-drawer-fixed.mud-drawer-mini.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-persistent:not(.mud-drawer-clipped-never), .mud-drawer-fixed.mud-drawer-responsive.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-temporary.mud-drawer-clipped-always {
 | 
			
		||||
    height: calc(100% - var(--mud-appbar-height) - 35px);
 | 
			
		||||
    height: calc(100% - var(--mud-appbar-height) - (var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.w-100 {
 | 
			
		||||
@@ -154,25 +150,91 @@ code {
 | 
			
		||||
    margin-right: 5px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.torrent-list .mud-table-container {
 | 
			
		||||
    height: calc(100vh - 160px);
 | 
			
		||||
/*. Layout helpers */
 | 
			
		||||
.content-panel {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.file-list .mud-table-container {
 | 
			
		||||
    height: calc(100vh - 245px);
 | 
			
		||||
.content-panel__toolbar {
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.details-list .mud-table-container {
 | 
			
		||||
    height: calc(100vh - 200px);
 | 
			
		||||
.content-panel__toolbar--scroll {
 | 
			
		||||
    overflow-x: auto;
 | 
			
		||||
    white-space: nowrap;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.details-tab-contents {
 | 
			
		||||
    height: calc(100vh - 200px);
 | 
			
		||||
.content-panel__body {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-panel__container {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-panel__table {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-panel__table .mud-table-container {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-panel__body > .mud-tabs {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    padding-top: 0;
 | 
			
		||||
    margin-top: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
    .content-panel__body > .mud-tabs .mud-tabs-tabbar {
 | 
			
		||||
        margin-bottom: 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .content-panel__body > .mud-tabs .mud-tabs-panels {
 | 
			
		||||
        flex: 1 1 auto;
 | 
			
		||||
        min-height: 0;
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: column;
 | 
			
		||||
        overflow: hidden;
 | 
			
		||||
        padding-top: 0;
 | 
			
		||||
        margin-top: -1px;
 | 
			
		||||
        border-top: none;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
.content-panel__body .mud-tabs .mud-tabs-panels .mud-tab-panel {
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.torrent-list .mud-table-container,
 | 
			
		||||
.file-list .mud-table-container,
 | 
			
		||||
.details-list .mud-table-container,
 | 
			
		||||
.search-list .mud-table-container {
 | 
			
		||||
    height: calc(100vh - 260px);
 | 
			
		||||
    height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.details-tab-contents,
 | 
			
		||||
.options-tab-contents,
 | 
			
		||||
.rss-contents {
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    overflow: auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
tr.log-normal td {
 | 
			
		||||
@@ -220,10 +282,6 @@ td .folder-button {
 | 
			
		||||
    padding: 6px 16px 6px 16px !important;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.rss-contents {
 | 
			
		||||
    height: calc(100vh - 149px);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@keyframes spin {
 | 
			
		||||
    0% {
 | 
			
		||||
        transform: rotate(0deg);
 | 
			
		||||
@@ -256,3 +314,116 @@ td .folder-button {
 | 
			
		||||
.mud-popover .mud-divider:last-child {
 | 
			
		||||
    display: none;
 | 
			
		||||
}
 | 
			
		||||
:root {
 | 
			
		||||
    --app-viewport-height: 100vh;
 | 
			
		||||
    --app-status-bar-height: 35px;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports (height: 100svh) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --app-viewport-height: 100svh;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports ((height: 100dvh) and (not (height: 100svh))) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --app-viewport-height: 100dvh;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
html,
 | 
			
		||||
body {
 | 
			
		||||
    height: var(--app-viewport-height);
 | 
			
		||||
    min-height: var(--app-viewport-height);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
body {
 | 
			
		||||
    margin: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    overscroll-behavior: none;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#app,
 | 
			
		||||
.mud-layout {
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    min-height: 100%;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    height: var(--app-viewport-height);
 | 
			
		||||
    min-height: var(--app-viewport-height);
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell__body {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell__sidebar {
 | 
			
		||||
    flex: 0 0 auto;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell__main {
 | 
			
		||||
    display: flex;
 | 
			
		||||
    flex-direction: column;
 | 
			
		||||
    flex: 1 1 auto;
 | 
			
		||||
    min-height: 0;
 | 
			
		||||
    overflow: hidden;
 | 
			
		||||
    padding: var(--mud-appbar-height) 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell__status-bar.mud-appbar {
 | 
			
		||||
    flex: 0 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
 | 
			
		||||
    height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    background-color: var(--mud-palette-dark-lighten);
 | 
			
		||||
    align-items: center;
 | 
			
		||||
    justify-content: flex-start;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.app-shell__status-bar .mud-toolbar {
 | 
			
		||||
    width: 100%;
 | 
			
		||||
    height: 100%;
 | 
			
		||||
    padding-bottom: env(safe-area-inset-bottom, 0px);
 | 
			
		||||
    background-color: inherit;
 | 
			
		||||
    box-sizing: border-box;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@supports (-webkit-touch-callout: none) {
 | 
			
		||||
    :root {
 | 
			
		||||
        --app-viewport-height: -webkit-fill-available;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    html,
 | 
			
		||||
    body {
 | 
			
		||||
        height: -webkit-fill-available;
 | 
			
		||||
        min-height: -webkit-fill-available;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    .app-shell {
 | 
			
		||||
        height: -webkit-fill-available;
 | 
			
		||||
        min-height: -webkit-fill-available;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
/* Tab bar gap fix */
 | 
			
		||||
.content-panel__body > .mud-tabs .mud-tabs-tabbar {
 | 
			
		||||
    margin-bottom: 0;
 | 
			
		||||
    padding-bottom: 0;
 | 
			
		||||
    border-bottom-width: 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-wrapper {
 | 
			
		||||
    margin-bottom: -1px;
 | 
			
		||||
}
 | 
			
		||||
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-slider {
 | 
			
		||||
    bottom: 0;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,4 +5,4 @@
 | 
			
		||||
// * @author John Doherty <www.johndoherty.info>
 | 
			
		||||
// * @license MIT
 | 
			
		||||
// */
 | 
			
		||||
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e), this.dispatchEvent(new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY })) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
 | 
			
		||||
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e); var n = new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY }); n.__longPress = !0, this.dispatchEvent(n) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
 | 
			
		||||
@@ -27,7 +27,7 @@ namespace Lantean.QBitTorrentClient.Converters
 | 
			
		||||
            {
 | 
			
		||||
                writer.WriteNumberValue(0);
 | 
			
		||||
            }
 | 
			
		||||
            else if (value.IsDefaltFolder)
 | 
			
		||||
            else if (value.IsDefaultFolder)
 | 
			
		||||
            {
 | 
			
		||||
                writer.WriteNumberValue(1);
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -112,7 +112,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
			
		||||
            int maxConnecPerTorrent,
 | 
			
		||||
            int maxInactiveSeedingTime,
 | 
			
		||||
            bool maxInactiveSeedingTimeEnabled,
 | 
			
		||||
            int maxRatio,
 | 
			
		||||
            float maxRatio,
 | 
			
		||||
            int maxRatioAct,
 | 
			
		||||
            bool maxRatioEnabled,
 | 
			
		||||
            int maxSeedingTime,
 | 
			
		||||
@@ -745,7 +745,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
			
		||||
        public bool MaxInactiveSeedingTimeEnabled { get; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("max_ratio")]
 | 
			
		||||
        public int MaxRatio { get; }
 | 
			
		||||
        public float MaxRatio { get; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("max_ratio_act")]
 | 
			
		||||
        public int MaxRatioAct { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
    {
 | 
			
		||||
        public bool IsWatchedFolder { get; set; }
 | 
			
		||||
 | 
			
		||||
        public bool IsDefaltFolder { get; set; }
 | 
			
		||||
        public bool IsDefaultFolder { get; set; }
 | 
			
		||||
 | 
			
		||||
        public string? SavePath { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +23,7 @@
 | 
			
		||||
                {
 | 
			
		||||
                    return new SaveLocation
 | 
			
		||||
                    {
 | 
			
		||||
                        IsDefaltFolder = true
 | 
			
		||||
                        IsDefaultFolder = true
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
@@ -40,7 +40,7 @@
 | 
			
		||||
                {
 | 
			
		||||
                    return new SaveLocation
 | 
			
		||||
                    {
 | 
			
		||||
                        IsDefaltFolder = true
 | 
			
		||||
                        IsDefaultFolder = true
 | 
			
		||||
                    };
 | 
			
		||||
                }
 | 
			
		||||
                else
 | 
			
		||||
@@ -61,7 +61,7 @@
 | 
			
		||||
            {
 | 
			
		||||
                return 0;
 | 
			
		||||
            }
 | 
			
		||||
            else if (IsDefaltFolder)
 | 
			
		||||
            else if (IsDefaultFolder)
 | 
			
		||||
            {
 | 
			
		||||
                return 1;
 | 
			
		||||
            }
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
			
		||||
            long downloadLimit,
 | 
			
		||||
            long downloadSpeed,
 | 
			
		||||
            long downloadSpeedAverage,
 | 
			
		||||
            int estimatedTimeOfArrival,
 | 
			
		||||
            long estimatedTimeOfArrival,
 | 
			
		||||
            long lastSeen,
 | 
			
		||||
            int connections,
 | 
			
		||||
            int connectionsLimit,
 | 
			
		||||
@@ -104,7 +104,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
			
		||||
        public long DownloadSpeedAverage { get; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("eta")]
 | 
			
		||||
        public int EstimatedTimeOfArrival { get; }
 | 
			
		||||
        public long EstimatedTimeOfArrival { get; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("last_seen")]
 | 
			
		||||
        public long LastSeen { get; }
 | 
			
		||||
 
 | 
			
		||||
@@ -323,7 +323,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
			
		||||
        public bool? MaxInactiveSeedingTimeEnabled { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("max_ratio")]
 | 
			
		||||
        public int? MaxRatio { get; set; }
 | 
			
		||||
        public float? MaxRatio { get; set; }
 | 
			
		||||
 | 
			
		||||
        [JsonPropertyName("max_ratio_act")]
 | 
			
		||||
        public int? MaxRatioAct { get; set; }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										5
									
								
								global.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								global.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
{
 | 
			
		||||
  "sdk": {
 | 
			
		||||
    "version": "9.0.306"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -68,11 +68,13 @@ cd qbtmud
 | 
			
		||||
dotnet restore
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
### 3. Build the Application
 | 
			
		||||
### 3. Build and Publish the Application
 | 
			
		||||
```sh
 | 
			
		||||
dotnet build --configuration Release
 | 
			
		||||
dotnet publish --configuration Release
 | 
			
		||||
```
 | 
			
		||||
 | 
			
		||||
This will output the Web UI files to `Lantean.QBTMud\bin\Release\net9.0\publish\wwwroot`.
 | 
			
		||||
 | 
			
		||||
### 4. Configure qBittorrent to Use qbtmud
 | 
			
		||||
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user