mirror of
				https://github.com/lantean-code/qbtmud.git
				synced 2025-11-04 05:53:22 +00:00 
			
		
		
		
	Compare commits
	
		
			56 Commits
		
	
	
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					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 | ||
| 
						 | 
					1cf9f97187 | ||
| 
						 | 
					4f9129fd46 | ||
| 
						 | 
					9a9d2c2ee2 | ||
| 
						 | 
					736bc46745 | ||
| 
						 | 
					23ae19c4c7 | ||
| 
						 | 
					603470eb30 | ||
| 
						 | 
					27c2406340 | ||
| 
						 | 
					4578dcc11f | ||
| 
						 | 
					3215fa3936 | ||
| 
						 | 
					78e62f31d0 | ||
| 
						 | 
					e23842fcb0 | ||
| 
						 | 
					411c7f87cc | ||
| 
						 | 
					4098f8f5a9 | ||
| 
						 | 
					12f81c5978 | ||
| 
						 | 
					717738d720 | ||
| 
						 | 
					885c34c8cf | ||
| 
						 | 
					ef3c68a6aa | ||
| 
						 | 
					a29e64fc1b | ||
| 
						 | 
					e55955c75e | ||
| 
						 | 
					aa80396862 | ||
| 
						 | 
					30ced3293c | ||
| 
						 | 
					c54f73a517 | ||
| 
						 | 
					bad509e40f | ||
| 
						 | 
					6a0796ef20 | ||
| 
						 | 
					dc4b515763 | ||
| 
						 | 
					938702a7b3 | ||
| 
						 | 
					6ca1c6edd4 | ||
| 
						 | 
					24eb5cf5e9 | 
							
								
								
									
										4
									
								
								.github/workflows/dotnet.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/dotnet.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,12 +21,12 @@ jobs:
 | 
				
			|||||||
      - name: Setup .NET
 | 
					      - name: Setup .NET
 | 
				
			||||||
        uses: actions/setup-dotnet@v4
 | 
					        uses: actions/setup-dotnet@v4
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          dotnet-version: '8.0.x'
 | 
					          dotnet-version: '9.0.x'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Install GitVersion
 | 
					      - name: Install GitVersion
 | 
				
			||||||
        uses: gittools/actions/gitversion/setup@v3.0.0
 | 
					        uses: gittools/actions/gitversion/setup@v3.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          versionSpec: '6.x'
 | 
					          versionSpec: '6.0.0'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Determine Version
 | 
					      - name: Determine Version
 | 
				
			||||||
        id: gitversion
 | 
					        id: gitversion
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -361,3 +361,4 @@ MigrationBackup/
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Fody - auto-generated XML schema
 | 
					# Fody - auto-generated XML schema
 | 
				
			||||||
FodyWeavers.xsd
 | 
					FodyWeavers.xsd
 | 
				
			||||||
 | 
					/output
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,23 @@
 | 
				
			|||||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
					<Project Sdk="Microsoft.NET.Sdk">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <TargetFramework>net8.0</TargetFramework>
 | 
					    <TargetFramework>net9.0</TargetFramework>
 | 
				
			||||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
					    <ImplicitUsings>enable</ImplicitUsings>
 | 
				
			||||||
    <Nullable>enable</Nullable>
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
 | 
					    <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
				
			||||||
    <IsPackable>false</IsPackable>
 | 
					    <IsPackable>false</IsPackable>
 | 
				
			||||||
    <IsTestProject>true</IsTestProject>
 | 
					    <IsTestProject>true</IsTestProject>
 | 
				
			||||||
  </PropertyGroup>
 | 
					  </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
    <PackageReference Include="FluentAssertions" Version="6.12.1" />
 | 
					    <PackageReference Include="AwesomeAssertions" Version="9.2.1" />
 | 
				
			||||||
    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
 | 
					    <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
 | 
				
			||||||
    <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
 | 
					    <PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
 | 
				
			||||||
    <PackageReference Include="xunit" Version="2.9.2" />
 | 
					    <PackageReference Include="xunit" Version="2.9.3" />
 | 
				
			||||||
    <PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
 | 
					    <PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
 | 
				
			||||||
      <PrivateAssets>all</PrivateAssets>
 | 
					      <PrivateAssets>all</PrivateAssets>
 | 
				
			||||||
      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
					      <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
 | 
				
			||||||
    </PackageReference>
 | 
					    </PackageReference>
 | 
				
			||||||
	  <PackageReference Include="System.Net.Http" Version="4.3.4" />
 | 
					 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
using Lantean.QBitTorrentClient;
 | 
					using Lantean.QBitTorrentClient;
 | 
				
			||||||
using Lantean.QBitTorrentClient.Models;
 | 
					using Lantean.QBitTorrentClient.Models;
 | 
				
			||||||
using System.Linq.Expressions;
 | 
					using System.Linq.Expressions;
 | 
				
			||||||
using System.Text.Json;
 | 
					using System.Text.Json;
 | 
				
			||||||
@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Test
 | 
				
			|||||||
            Test2(a => a.Name);
 | 
					            Test2(a => a.Name);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private void Test2(Expression<Func<TestClass, object>> expr)
 | 
					        private void Test2(Expression<Func<TestClass, object?>> expr)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var body = expr.Body;
 | 
					            var body = expr.Body;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -38,7 +38,7 @@ namespace Lantean.QBTMud.Test
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
 | 
					            var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Expression<Func<TestClass, object>> expr2 = a => a.Name;
 | 
					            Expression<Func<TestClass, object?>> expr2 = a => a.Name;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var x = l.Compile();
 | 
					            var x = l.Compile();
 | 
				
			||||||
            var res = (long)x(new TestClass { Name = "Name", Value = 12 });
 | 
					            var res = (long)x(new TestClass { Name = "Name", Value = 12 });
 | 
				
			||||||
@@ -58,9 +58,9 @@ namespace Lantean.QBTMud.Test
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public class TestClass
 | 
					    public class TestClass
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        public string Name { get; set; }
 | 
					        public string? Name { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string Description { get; set; }
 | 
					        public string? Description { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public long Value { get; set; }
 | 
					        public long Value { get; set; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class AddPeerDialog
 | 
					    public partial class AddPeerDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        public IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected HashSet<PeerId> Peers { get; } = [];
 | 
					        protected HashSet<PeerId> Peers { get; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
					        protected IDialogService DialogService { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected HashSet<string> Tags { get; } = [];
 | 
					        protected HashSet<string> Tags { get; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class AddTorrentFileDialog
 | 
					    public partial class AddTorrentFileDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
 | 
					        protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        protected IKeyboardService KeyboardService { get; set; } = default!;
 | 
					        protected IKeyboardService KeyboardService { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Url { get; set; }
 | 
					        public string? Url { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
<MudGrid>
 | 
					<MudGrid>
 | 
				
			||||||
    <MudItem xs="12">
 | 
					    <MudItem xs="12">
 | 
				
			||||||
        <MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPosition="LabelPosition.End" />
 | 
					        <MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
 | 
				
			||||||
    </MudItem>
 | 
					    </MudItem>
 | 
				
			||||||
</MudGrid>
 | 
					</MudGrid>
 | 
				
			||||||
<MudCollapse Expanded="Expanded">
 | 
					<MudCollapse Expanded="Expanded">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class AddTrackerDialog
 | 
					    public partial class AddTrackerDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected HashSet<string> Trackers { get; } = [];
 | 
					        protected HashSet<string> Trackers { get; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        private string _savePath = string.Empty;
 | 
					        private string _savePath = string.Empty;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Inject]
 | 
					        [Inject]
 | 
				
			||||||
        protected IApiClient ApiClient { get; set; } = default!;
 | 
					        protected IApiClient ApiClient { get; set; } = default!;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,12 +5,13 @@
 | 
				
			|||||||
    <DialogContent>
 | 
					    <DialogContent>
 | 
				
			||||||
        <MudCard Class="w-100" Elevation="0">
 | 
					        <MudCard Class="w-100" Elevation="0">
 | 
				
			||||||
            <MudGrid>
 | 
					            <MudGrid>
 | 
				
			||||||
                @for (var i = 0; i < Columns.Count; i++)
 | 
					                @for (var i = 0; i < OrderedColumns.Length; i++)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    var column = Columns[i];
 | 
					                    var item = OrderedColumns[i];
 | 
				
			||||||
 | 
					                    var column = Columns.First(c => c.Id == item);
 | 
				
			||||||
                    var index = i;
 | 
					                    var index = i;
 | 
				
			||||||
                    <MudItem xs="7">
 | 
					                    <MudItem xs="7">
 | 
				
			||||||
                            <MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
 | 
					                        <MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPlacement="Placement.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
 | 
				
			||||||
                    </MudItem>
 | 
					                    </MudItem>
 | 
				
			||||||
                    <MudItem xs="3">
 | 
					                    <MudItem xs="3">
 | 
				
			||||||
                        <MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
 | 
					                        <MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class ColumnOptionsDialog<T>
 | 
					    public partial class ColumnOptionsDialog<T>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        [EditorRequired]
 | 
					        [EditorRequired]
 | 
				
			||||||
@@ -20,10 +20,15 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public Dictionary<string, int?> Widths { get; set; } = [];
 | 
					        public Dictionary<string, int?> Widths { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        [Parameter]
 | 
				
			||||||
 | 
					        public Dictionary<string, int> Order { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
 | 
					        protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
 | 
					        protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        protected Dictionary<string, int> OrderInternal { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override void OnParametersSet()
 | 
					        protected override void OnParametersSet()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (SelectedColumnsInternal.Count == 0)
 | 
					            if (SelectedColumnsInternal.Count == 0)
 | 
				
			||||||
@@ -51,6 +56,25 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
                    WidthsInternal[width.Key] = width.Value;
 | 
					                    WidthsInternal[width.Key] = width.Value;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (OrderInternal.Count == 0)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                if (Order.Count == 0)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    for (int i = 0; i < Columns.Count; i++)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        var column = Columns[i];
 | 
				
			||||||
 | 
					                        OrderInternal.Add(column.Id, i);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    foreach (var order in Order)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        OrderInternal[order.Key] = order.Value;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void SetSelected(bool selected, string id)
 | 
					        protected void SetSelected(bool selected, string id)
 | 
				
			||||||
@@ -101,7 +125,15 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            (Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
 | 
					            var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
 | 
				
			||||||
 | 
					            var otherId = OrderInternal.FirstOrDefault(o => o.Value == index - 1).Key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            OrderInternal[otherId] = index;
 | 
				
			||||||
 | 
					            OrderInternal[currentId] = index - 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            StateHasChanged();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void MoveDown(int index)
 | 
					        protected void MoveDown(int index)
 | 
				
			||||||
@@ -111,7 +143,15 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            (Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
 | 
					            var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
 | 
				
			||||||
 | 
					            var otherId = OrderInternal.FirstOrDefault(o => o.Value == index + 1).Key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            OrderInternal[otherId] = index;
 | 
				
			||||||
 | 
					            OrderInternal[currentId] = index + 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            //(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            StateHasChanged();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected string GetValue(int? value, string columnId)
 | 
					        protected string GetValue(int? value, string columnId)
 | 
				
			||||||
@@ -134,6 +174,13 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
            return value.Value.ToString();
 | 
					            return value.Value.ToString();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private string[] OrderedColumns => GetOrderedColumns();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private string[] GetOrderedColumns()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            return OrderInternal.OrderBy(x => x.Value).Select(x => x.Key).ToArray();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void Cancel()
 | 
					        protected void Cancel()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            MudDialog.Cancel();
 | 
					            MudDialog.Cancel();
 | 
				
			||||||
@@ -141,7 +188,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected void Submit()
 | 
					        protected void Submit()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal)));
 | 
					            MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal, OrderInternal)));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override Task Submit(KeyboardEvent keyboardEvent)
 | 
					        protected override Task Submit(KeyboardEvent keyboardEvent)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class ConfirmDialog
 | 
					    public partial class ConfirmDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string Content { get; set; } = default!;
 | 
					        public string Content { get; set; } = default!;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        <MudGrid>
 | 
					        <MudGrid>
 | 
				
			||||||
            <MudItem xs="12">
 | 
					            <MudItem xs="12">
 | 
				
			||||||
                <MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPosition="LabelPosition.End" />
 | 
					                <MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPlacement="Placement.End" />
 | 
				
			||||||
            </MudItem>
 | 
					            </MudItem>
 | 
				
			||||||
        </MudGrid>
 | 
					        </MudGrid>
 | 
				
			||||||
    </DialogContent>
 | 
					    </DialogContent>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class DeleteDialog
 | 
					    public partial class DeleteDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public int Count { get; set; }
 | 
					        public int Count { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class ExceptionDialog
 | 
					    public partial class ExceptionDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public Exception? Exception { get; set; }
 | 
					        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);
 | 
					        private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected IReadOnlyList<PropertyInfo> Columns => _properties;
 | 
					        protected IReadOnlyList<PropertyInfo> Columns => _properties;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
					        protected IDialogService DialogService { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public IEnumerable<string> Hashes { get; set; } = [];
 | 
					        public IEnumerable<string> Hashes { get; set; } = [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
					        protected IDialogService DialogService { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public IEnumerable<string> Hashes { get; set; } = [];
 | 
					        public IEnumerable<string> Hashes { get; set; } = [];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class MultipleFieldDialog
 | 
					    public partial class MultipleFieldDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string Label { get; set; } = default!;
 | 
					        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>
 | 
					    public partial class NumericFieldDialog<T> where T : struct, INumber<T>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Label { get; set; }
 | 
					        public string? Label { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ using Lantean.QBTMud.Services;
 | 
				
			|||||||
using Microsoft.AspNetCore.Components;
 | 
					using Microsoft.AspNetCore.Components;
 | 
				
			||||||
using MudBlazor;
 | 
					using MudBlazor;
 | 
				
			||||||
using System.Collections.ObjectModel;
 | 
					using System.Collections.ObjectModel;
 | 
				
			||||||
using static MudBlazor.Colors;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace Lantean.QBTMud.Components.Dialogs
 | 
					namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
@@ -31,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        protected ILocalStorageService LocalStorage { get; set; } = default!;
 | 
					        protected ILocalStorageService LocalStorage { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Hash { get; set; }
 | 
					        public string? Hash { get; set; }
 | 
				
			||||||
@@ -427,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
 | 
					                await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override async Task OnInitializedAsync()
 | 
					        protected override async Task OnInitializedAsync()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
        private readonly List<string> _unsavedRuleNames = [];
 | 
					        private readonly List<string> _unsavedRuleNames = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Inject]
 | 
					        [Inject]
 | 
				
			||||||
        protected IDialogService DialogService { get; set; } = default!;
 | 
					        protected IDialogService DialogService { get; set; } = default!;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class ShareRatioDialog
 | 
					    public partial class ShareRatioDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Label { get; set; }
 | 
					        public string? Label { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class SliderFieldDialog<T> where T : struct, INumber<T>
 | 
					    public partial class SliderFieldDialog<T> where T : struct, INumber<T>
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Label { get; set; }
 | 
					        public string? Label { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class StringFieldDialog
 | 
					    public partial class StringFieldDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public string? Label { get; set; }
 | 
					        public string? Label { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class SubMenuDialog
 | 
					    public partial class SubMenuDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public UIAction? ParentAction { get; set; }
 | 
					        public UIAction? ParentAction { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
 | 
				
			|||||||
    public partial class TorrentOptionsDialog
 | 
					    public partial class TorrentOptionsDialog
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MudDialogInstance MudDialog { get; set; } = default!;
 | 
					        private IMudDialogInstance MudDialog { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        [EditorRequired]
 | 
					        [EditorRequired]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,9 @@
 | 
				
			|||||||
<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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
 | 
				
			||||||
</ContextMenu>
 | 
					</MudMenu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
 | 
					            <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
@@ -22,7 +24,8 @@
 | 
				
			|||||||
            <MudSpacer />
 | 
					            <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>
 | 
					            <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>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <DynamicTable
 | 
					        <DynamicTable
 | 
				
			||||||
            @ref="Table"
 | 
					            @ref="Table"
 | 
				
			||||||
            T="ContentItem" 
 | 
					            T="ContentItem" 
 | 
				
			||||||
@@ -36,8 +39,10 @@
 | 
				
			|||||||
            SortDirectionChanged="SortDirectionChanged"
 | 
					            SortDirectionChanged="SortDirectionChanged"
 | 
				
			||||||
            OnTableDataContextMenu="TableDataContextMenu"
 | 
					            OnTableDataContextMenu="TableDataContextMenu"
 | 
				
			||||||
            OnTableDataLongPress="TableDataLongPress"
 | 
					            OnTableDataLongPress="TableDataLongPress"
 | 
				
			||||||
    Class="file-list"
 | 
					            Class="file-list content-panel__table"
 | 
				
			||||||
        />
 | 
					        />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@code {
 | 
					@code {
 | 
				
			||||||
    private RenderFragment<RowContext<ContentItem>> NameColumn
 | 
					    private RenderFragment<RowContext<ContentItem>> NameColumn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,6 +20,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private readonly CancellationTokenSource _timerCancellationToken = new();
 | 
					        private readonly CancellationTokenSource _timerCancellationToken = new();
 | 
				
			||||||
        private bool _disposedValue;
 | 
					        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 List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
 | 
				
			||||||
        private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
 | 
					        private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
 | 
				
			||||||
@@ -65,7 +68,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private DynamicTable<ContentItem>? Table { get; set; }
 | 
					        private DynamicTable<ContentItem>? Table { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private ContextMenu? ContextMenu { get; set; }
 | 
					        private MudMenu? ContextMenu { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public FilesTab()
 | 
					        public FilesTab()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -102,6 +105,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            if (_filterDefinitions is null)
 | 
					            if (_filterDefinitions is null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                Filters = null;
 | 
					                Filters = null;
 | 
				
			||||||
 | 
					                MarkFilesDirty();
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -113,11 +117,13 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            Filters = filters;
 | 
					            Filters = filters;
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void RemoveFilter()
 | 
					        protected void RemoveFilter()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Filters = null;
 | 
					            Filters = null;
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async ValueTask DisposeAsync()
 | 
					        public async ValueTask DisposeAsync()
 | 
				
			||||||
@@ -157,6 +163,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
        protected void SearchTextChanged(string value)
 | 
					        protected void SearchTextChanged(string value)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            SearchText = value;
 | 
					            SearchText = value;
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
 | 
					        protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
 | 
				
			||||||
@@ -178,7 +185,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await ContextMenu.OpenMenuAsync(eventArgs);
 | 
					            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
					        protected override async Task OnAfterRenderAsync(bool firstRender)
 | 
				
			||||||
@@ -197,6 +206,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
 | 
					                while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
 | 
					                    var hasUpdates = false;
 | 
				
			||||||
                    if (Active && Hash is not null)
 | 
					                    if (Active && Hash is not null)
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        IReadOnlyList<QBitTorrentClient.Models.FileData> files;
 | 
					                        IReadOnlyList<QBitTorrentClient.Models.FileData> files;
 | 
				
			||||||
@@ -213,17 +223,23 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                        if (FileList is null)
 | 
					                        if (FileList is null)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            FileList = DataManager.CreateContentsList(files);
 | 
					                            FileList = DataManager.CreateContentsList(files);
 | 
				
			||||||
 | 
					                            hasUpdates = true;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        else
 | 
					                        else
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            DataManager.MergeContentsList(files, FileList);
 | 
					                            hasUpdates = DataManager.MergeContentsList(files, FileList);
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (hasUpdates)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        MarkFilesDirty();
 | 
				
			||||||
 | 
					                        PruneSelectionIfMissing();
 | 
				
			||||||
                        await InvokeAsync(StateHasChanged);
 | 
					                        await InvokeAsync(StateHasChanged);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override async Task OnParametersSetAsync()
 | 
					        protected override async Task OnParametersSetAsync()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
@@ -246,6 +262,8 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            var contents = await ApiClient.GetTorrentContents(Hash);
 | 
					            var contents = await ApiClient.GetTorrentContents(Hash);
 | 
				
			||||||
            FileList = DataManager.CreateContentsList(contents);
 | 
					            FileList = DataManager.CreateContentsList(contents);
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
 | 
					            PruneSelectionIfMissing();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
 | 
					            var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
 | 
				
			||||||
            if (expandedNodes is not null)
 | 
					            if (expandedNodes is not null)
 | 
				
			||||||
@@ -256,6 +274,8 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                ExpandedNodes.Clear();
 | 
					                ExpandedNodes.Clear();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
 | 
					        protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
 | 
				
			||||||
@@ -320,11 +340,13 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
        protected void SortColumnChanged(string sortColumn)
 | 
					        protected void SortColumnChanged(string sortColumn)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _sortColumn = sortColumn;
 | 
					            _sortColumn = sortColumn;
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void SortDirectionChanged(SortDirection sortDirection)
 | 
					        protected void SortDirectionChanged(SortDirection sortDirection)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _sortDirection = sortDirection;
 | 
					            _sortDirection = sortDirection;
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void SelectedItemChanged(ContentItem item)
 | 
					        protected void SelectedItemChanged(ContentItem item)
 | 
				
			||||||
@@ -343,6 +365,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                ExpandedNodes.Add(contentItem.Name);
 | 
					                ExpandedNodes.Add(contentItem.Name);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            MarkFilesDirty();
 | 
				
			||||||
            await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
 | 
					            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);
 | 
					            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)
 | 
					        private bool FilterContentItem(ContentItem item)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (Filters is not null)
 | 
					            if (Filters is not null)
 | 
				
			||||||
@@ -429,38 +414,130 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private ReadOnlyCollection<ContentItem> GetFiles()
 | 
					        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)
 | 
					            if (FileList is null || FileList.Values.Count == 0)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return new ReadOnlyCollection<ContentItem>([]);
 | 
					                return EmptyContentItems;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var maxLevel = FileList.Values.Max(f => f.Level);
 | 
					            var lookup = BuildChildrenLookup();
 | 
				
			||||||
            // this is a flat file structure
 | 
					            if (!lookup.TryGetValue(string.Empty, out var roots))
 | 
				
			||||||
            if (maxLevel == 0)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                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 orderedRoots)
 | 
				
			||||||
            foreach (var item in rootItems)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                list.Add(item);
 | 
					                if (item.IsFolder)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    result.Add(item);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (item.IsFolder && ExpandedNodes.Contains(item.Name))
 | 
					                    if (!ExpandedNodes.Contains(item.Name))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                    var level = 0;
 | 
					                        continue;
 | 
				
			||||||
                    var descendants = GetChildren(item, level);
 | 
					                    }
 | 
				
			||||||
                    foreach (var descendant in descendants)
 | 
					
 | 
				
			||||||
 | 
					                    var descendants = GetVisibleDescendants(item, lookup, sortSelector);
 | 
				
			||||||
 | 
					                    result.AddRange(descendants);
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                else
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                        list.Add(descendant);
 | 
					                    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()
 | 
					        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)
 | 
					    @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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
 | 
				
			||||||
    @if (IsCategoryTarget)
 | 
					    @if (IsCategoryTarget)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -12,9 +12,9 @@
 | 
				
			|||||||
    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
 | 
				
			||||||
    <MudDivider />
 | 
					    <MudDivider />
 | 
				
			||||||
    @TorrentControls(_categoryType)
 | 
					    @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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
 | 
				
			||||||
    @if (IsTagTarget)
 | 
					    @if (IsTagTarget)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -23,13 +23,13 @@
 | 
				
			|||||||
    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
 | 
				
			||||||
    <MudDivider />
 | 
					    <MudDivider />
 | 
				
			||||||
    @TorrentControls(_tagType)
 | 
					    @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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
 | 
				
			||||||
    <MudDivider />
 | 
					    <MudDivider />
 | 
				
			||||||
    @TorrentControls(_trackerType)
 | 
					    @TorrentControls(_trackerType)
 | 
				
			||||||
</ContextMenu>
 | 
					</MudMenu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudNavMenu Dense="true">
 | 
					<MudNavMenu Dense="true">
 | 
				
			||||||
    <MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
 | 
					    <MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
using Blazored.LocalStorage;
 | 
					using Blazored.LocalStorage;
 | 
				
			||||||
using Lantean.QBitTorrentClient;
 | 
					using Lantean.QBitTorrentClient;
 | 
				
			||||||
using Lantean.QBTMud.Components.UI;
 | 
					 | 
				
			||||||
using Lantean.QBTMud.Helpers;
 | 
					using Lantean.QBTMud.Helpers;
 | 
				
			||||||
using Lantean.QBTMud.Models;
 | 
					using Lantean.QBTMud.Models;
 | 
				
			||||||
using Microsoft.AspNetCore.Components;
 | 
					using Microsoft.AspNetCore.Components;
 | 
				
			||||||
@@ -69,13 +68,13 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected Dictionary<string, int> Statuses => GetStatuses();
 | 
					        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; }
 | 
					        protected string? ContextMenuStatus { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -154,7 +153,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ContextMenuStatus = value;
 | 
					            ContextMenuStatus = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return StatusContextMenu.OpenMenuAsync(args);
 | 
					            var normalizedArgs = args.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return StatusContextMenu.OpenMenuAsync(normalizedArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task CategoryValueChanged(string value)
 | 
					        protected async Task CategoryValueChanged(string value)
 | 
				
			||||||
@@ -192,7 +193,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
 | 
					            IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
 | 
				
			||||||
            ContextMenuCategory = value;
 | 
					            ContextMenuCategory = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return CategoryContextMenu.OpenMenuAsync(args);
 | 
					            var normalizedArgs = args.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return CategoryContextMenu.OpenMenuAsync(normalizedArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task TagValueChanged(string value)
 | 
					        protected async Task TagValueChanged(string value)
 | 
				
			||||||
@@ -230,7 +233,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
            IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
 | 
					            IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
 | 
				
			||||||
            ContextMenuTag = value;
 | 
					            ContextMenuTag = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return TagContextMenu.OpenMenuAsync(args);
 | 
					            var normalizedArgs = args.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return TagContextMenu.OpenMenuAsync(normalizedArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task TrackerValueChanged(string value)
 | 
					        protected async Task TrackerValueChanged(string value)
 | 
				
			||||||
@@ -267,7 +272,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            ContextMenuTracker = value;
 | 
					            ContextMenuTracker = value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return TrackerContextMenu.OpenMenuAsync(args);
 | 
					            var normalizedArgs = args.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return TrackerContextMenu.OpenMenuAsync(normalizedArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task AddCategory()
 | 
					        protected async Task AddCategory()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -92,7 +92,9 @@
 | 
				
			|||||||
                <FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
 | 
					                <FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
 | 
				
			||||||
            </MudItem>
 | 
					            </MudItem>
 | 
				
			||||||
            <MudItem xs="9">
 | 
					            <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>
 | 
				
			||||||
            <MudItem xs="3">
 | 
					            <MudItem xs="3">
 | 
				
			||||||
                <FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
 | 
					                <FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -17,7 +17,7 @@
 | 
				
			|||||||
        protected int SlowTorrentUlRateThreshold { get; private set; }
 | 
					        protected int SlowTorrentUlRateThreshold { get; private set; }
 | 
				
			||||||
        protected int SlowTorrentInactiveTimer { get; private set; }
 | 
					        protected int SlowTorrentInactiveTimer { get; private set; }
 | 
				
			||||||
        protected bool MaxRatioEnabled { 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 bool MaxSeedingTimeEnabled { get; private set; }
 | 
				
			||||||
        protected int MaxSeedingTime { get; private set; }
 | 
					        protected int MaxSeedingTime { get; private set; }
 | 
				
			||||||
        protected int MaxRatioAct { get; private set; }
 | 
					        protected int MaxRatioAct { get; private set; }
 | 
				
			||||||
@@ -275,7 +275,7 @@
 | 
				
			|||||||
            await PreferencesChanged.InvokeAsync(UpdatePreferences);
 | 
					            await PreferencesChanged.InvokeAsync(UpdatePreferences);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task MaxRatioChanged(int value)
 | 
					        protected async Task MaxRatioChanged(float value)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            MaxRatio = value;
 | 
					            MaxRatio = value;
 | 
				
			||||||
            UpdatePreferences.MaxRatio = value;
 | 
					            UpdatePreferences.MaxRatio = value;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -62,7 +62,7 @@
 | 
				
			|||||||
    <MudCardContent Class="pt-0">
 | 
					    <MudCardContent Class="pt-0">
 | 
				
			||||||
        <MudGrid>
 | 
					        <MudGrid>
 | 
				
			||||||
            <MudItem xs="12">
 | 
					            <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="false">Manual</MudSelectItem>
 | 
				
			||||||
                    <MudSelectItem Value="true">Automatic</MudSelectItem>
 | 
					                    <MudSelectItem Value="true">Automatic</MudSelectItem>
 | 
				
			||||||
                </MudSelect>
 | 
					                </MudSelect>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,18 +1,22 @@
 | 
				
			|||||||
<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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
 | 
				
			||||||
    @if (ContextMenuItem is not null)
 | 
					    @if (ContextMenuItem is not null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        <MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
 | 
					        <MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
</ContextMenu>
 | 
					</MudMenu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
 | 
					            <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>
 | 
					            <MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <DynamicTable T="Peer"
 | 
					        <DynamicTable T="Peer"
 | 
				
			||||||
                      ColumnDefinitions="Columns"
 | 
					                      ColumnDefinitions="Columns"
 | 
				
			||||||
                      Items="Peers"
 | 
					                      Items="Peers"
 | 
				
			||||||
@@ -21,4 +25,6 @@
 | 
				
			|||||||
                      OnTableDataLongPress="TableDataLongPress"
 | 
					                      OnTableDataLongPress="TableDataLongPress"
 | 
				
			||||||
                      OnTableDataContextMenu="TableDataContextMenu"
 | 
					                      OnTableDataContextMenu="TableDataContextMenu"
 | 
				
			||||||
                      SelectedItemChanged="SelectedItemChanged"
 | 
					                      SelectedItemChanged="SelectedItemChanged"
 | 
				
			||||||
              Class="details-list" />
 | 
					                      Class="details-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected Peer? SelectedItem { get; set; }
 | 
					        protected Peer? SelectedItem { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected ContextMenu? ContextMenu { get; set; }
 | 
					        protected MudMenu? ContextMenu { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected DynamicTable<Peer>? Table { get; set; }
 | 
					        protected DynamicTable<Peer>? Table { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -153,7 +153,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await ContextMenu.ToggleMenuAsync(eventArgs);
 | 
					            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void SelectedItemChanged(Peer peer)
 | 
					        protected void SelectedItemChanged(Peer peer)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -191,13 +191,13 @@ else if (RenderType == RenderType.MenuItems)
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                if (!action.Children.Any())
 | 
					                if (!action.Children.Any())
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">
 | 
					                    <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled" Class="icon-menu-dense">
 | 
				
			||||||
                        @action.Text
 | 
					                        @action.Text
 | 
				
			||||||
                    </MudMenuItem>
 | 
					                    </MudMenuItem>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else
 | 
					                else
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))">
 | 
					                    <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))" Class="icon-menu-dense">
 | 
				
			||||||
                        <MudMenu ListClass="unselectable" Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" Ripple="false" Class="sub-menu">
 | 
					                        <MudMenu ListClass="unselectable" Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" Ripple="false" Class="sub-menu">
 | 
				
			||||||
                            <ActivatorContent>
 | 
					                            <ActivatorContent>
 | 
				
			||||||
                                @action.Text
 | 
					                                @action.Text
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,10 +12,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public partial class TorrentActions : IAsyncDisposable
 | 
					    public partial class TorrentActions : IAsyncDisposable
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        private const int _defaultVersion = 5;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        private bool _disposedValue;
 | 
					        private bool _disposedValue;
 | 
				
			||||||
        private int? _version;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private List<UIAction>? _actions;
 | 
					        private List<UIAction>? _actions;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -63,7 +60,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
        public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
 | 
					        public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public MudDialogInstance? MudDialog { get; set; }
 | 
					        public IMudDialogInstance? MudDialog { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Parameter]
 | 
					        [Parameter]
 | 
				
			||||||
        public UIAction? ParentAction { get; set; }
 | 
					        public UIAction? ParentAction { get; set; }
 | 
				
			||||||
@@ -74,37 +71,14 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected bool OverlayVisible { get; set; }
 | 
					        protected bool OverlayVisible { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected int MajorVersion
 | 
					        protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            get
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                if (_version is not null)
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    return _version.Value;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (string.IsNullOrEmpty(Version))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    return _defaultVersion;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                if (!System.Version.TryParse(Version.Replace("v", ""), out var version))
 | 
					 | 
				
			||||||
                {
 | 
					 | 
				
			||||||
                    return _defaultVersion;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                _version = version.Major;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                return _version.Value;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected override void OnInitialized()
 | 
					        protected override void OnInitialized()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _actions =
 | 
					            _actions =
 | 
				
			||||||
            [
 | 
					            [
 | 
				
			||||||
                new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
 | 
					                new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
 | 
				
			||||||
                new("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
 | 
					                new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
 | 
				
			||||||
                new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
 | 
					                new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
 | 
				
			||||||
                new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
 | 
					                new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
 | 
				
			||||||
                new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
 | 
					                new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
 | 
				
			||||||
@@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                    thereAreFirstLastPiecePrio = true;
 | 
					                    thereAreFirstLastPiecePrio = true;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (torrent.Progress != 1.0) // not downloaded
 | 
					                if (torrent.Progress < 0.999999) // not downloaded
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    allAreDownloaded = false;
 | 
					                    allAreDownloaded = false;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
 | 
				
			||||||
    @if (ContextMenuItem is not null)
 | 
					    @if (ContextMenuItem is not null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -6,8 +6,10 @@
 | 
				
			|||||||
        <MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
 | 
					        <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>
 | 
					        <MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
</ContextMenu>
 | 
					</MudMenu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
 | 
					            <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.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
 | 
				
			||||||
@@ -16,7 +18,9 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <DynamicTable @ref="Table"
 | 
					        <DynamicTable @ref="Table"
 | 
				
			||||||
                      T="Lantean.QBitTorrentClient.Models.TorrentTracker"
 | 
					                      T="Lantean.QBitTorrentClient.Models.TorrentTracker"
 | 
				
			||||||
                      ColumnDefinitions="Columns"
 | 
					                      ColumnDefinitions="Columns"
 | 
				
			||||||
@@ -29,4 +33,6 @@
 | 
				
			|||||||
                      OnTableDataLongPress="TableDataLongPress"
 | 
					                      OnTableDataLongPress="TableDataLongPress"
 | 
				
			||||||
                      OnTableDataContextMenu="TableDataContextMenu"
 | 
					                      OnTableDataContextMenu="TableDataContextMenu"
 | 
				
			||||||
                      SelectedItemChanged="SelectedItemChanged"
 | 
					                      SelectedItemChanged="SelectedItemChanged"
 | 
				
			||||||
              Class="file-list" />
 | 
					                      Class="file-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected TorrentTracker? SelectedItem { get; set; }
 | 
					        protected TorrentTracker? SelectedItem { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected ContextMenu? ContextMenu { get; set; }
 | 
					        protected MudMenu? ContextMenu { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected DynamicTable<TorrentTracker>? Table { get; set; }
 | 
					        protected DynamicTable<TorrentTracker>? Table { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -148,7 +148,9 @@ namespace Lantean.QBTMud.Components
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await ContextMenu.ToggleMenuAsync(eventArgs);
 | 
					            var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await ContextMenu.OpenMenuAsync(normalizedEventArgs);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void SelectedItemChanged(TorrentTracker torrentTracker)
 | 
					        protected void SelectedItemChanged(TorrentTracker torrentTracker)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +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="@FullWidth"
 | 
					 | 
				
			||||||
            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,294 +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
 | 
					 | 
				
			||||||
{
 | 
					 | 
				
			||||||
    // This is a very hacky approach but works for now.
 | 
					 | 
				
			||||||
    // This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked.
 | 
					 | 
				
			||||||
    // MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't.
 | 
					 | 
				
			||||||
    // Ideally this should be changed to use something like the way the DialogService works.
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Or - rework this to have a hidden MudMenu and hook into the OpenChanged event to monitor when the MudMenuItem closes it.
 | 
					 | 
				
			||||||
    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 bool FullWidth { 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 async Task OnAfterRenderAsync(bool firstRender)
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
            if (!_isResized)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                await DeterminePosition();
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        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 class="@Classname">
 | 
				
			||||||
    <div @onclick="EventUtil.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))
 | 
					        @if (!string.IsNullOrEmpty(Icon))
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            <MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
 | 
					            <MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,6 +59,7 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
            new CssBuilder("mud-nav-link")
 | 
					            new CssBuilder("mud-nav-link")
 | 
				
			||||||
                .AddClass($"mud-nav-link-disabled", Disabled)
 | 
					                .AddClass($"mud-nav-link-disabled", Disabled)
 | 
				
			||||||
                .AddClass("active", Active)
 | 
					                .AddClass("active", Active)
 | 
				
			||||||
 | 
					                .AddClass("unselectable", OnLongPress.HasDelegate || OnContextMenu.HasDelegate)
 | 
				
			||||||
                .Build();
 | 
					                .Build();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected string IconClassname =>
 | 
					        protected string IconClassname =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,6 +13,7 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
        private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
 | 
					        private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
 | 
				
			||||||
        private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
 | 
					        private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
 | 
				
			||||||
        private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
 | 
					        private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
 | 
				
			||||||
 | 
					        private readonly string _columnOrderStorageKey = $"DynamicTable{_typeName}.ColumnOrder";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Inject]
 | 
					        [Inject]
 | 
				
			||||||
        public ILocalStorageService LocalStorage { get; set; } = default!;
 | 
					        public ILocalStorageService LocalStorage { get; set; } = default!;
 | 
				
			||||||
@@ -80,14 +81,26 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected HashSet<string> SelectedColumns { get; set; } = [];
 | 
					        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?> _columnWidths = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private Dictionary<string, int> _columnOrder = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private string? _sortColumn;
 | 
					        private string? _sortColumn;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private SortDirection _sortDirection;
 | 
					        private SortDirection _sortDirection;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private DateTimeOffset? _suppressRowClickUntil;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private readonly Dictionary<string, TdExtended> _tds = [];
 | 
					        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()
 | 
					        protected override async Task OnInitializedAsync()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            HashSet<string> selectedColumns;
 | 
					            HashSet<string> selectedColumns;
 | 
				
			||||||
@@ -106,6 +119,13 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
                SelectedColumns = selectedColumns;
 | 
					                SelectedColumns = selectedColumns;
 | 
				
			||||||
                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
					                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                SelectedColumns = selectedColumns;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _lastColumnDefinitions = ColumnDefinitions;
 | 
				
			||||||
 | 
					            MarkColumnsDirty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            string? sortColumn;
 | 
					            string? sortColumn;
 | 
				
			||||||
            SortDirection sortDirection;
 | 
					            SortDirection sortDirection;
 | 
				
			||||||
@@ -134,11 +154,24 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
                await SortDirectionChanged.InvokeAsync(_sortDirection);
 | 
					                await SortDirectionChanged.InvokeAsync(_sortDirection);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            MarkColumnsDirty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
 | 
					            var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
 | 
				
			||||||
            if (storedColumnsWidths is not null)
 | 
					            if (storedColumnsWidths is not null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _columnWidths = storedColumnsWidths;
 | 
					                _columnWidths = storedColumnsWidths;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					            MarkColumnsDirty();
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        protected override void OnParametersSet()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            base.OnParametersSet();
 | 
				
			||||||
 | 
					            if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _lastColumnDefinitions = ColumnDefinitions;
 | 
				
			||||||
 | 
					                MarkColumnsDirty();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private IEnumerable<T>? GetOrderedItems()
 | 
					        private IEnumerable<T>? GetOrderedItems()
 | 
				
			||||||
@@ -162,18 +195,74 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
            return Items.OrderByDirection(_sortDirection, sortSelector);
 | 
					            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 (!_columnsDirty)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return _visibleColumns;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _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)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                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)
 | 
				
			||||||
 | 
					                {
 | 
				
			||||||
 | 
					                    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)
 | 
					                    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))
 | 
					                if (_columnWidths.TryGetValue(column.Id, out var value))
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    column.Width = value;
 | 
					                    column.Width = value;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					 | 
				
			||||||
                yield return column;
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return orderedColumns;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private async Task SetSort(string columnId, SortDirection sortDirection)
 | 
					        private async Task SetSort(string columnId, SortDirection sortDirection)
 | 
				
			||||||
@@ -199,6 +288,17 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
 | 
					        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)
 | 
					            if (eventArgs.Item is null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return;
 | 
					                return;
 | 
				
			||||||
@@ -274,13 +374,14 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
 | 
					        protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            _suppressRowClickUntil = DateTimeOffset.UtcNow.AddMilliseconds(500);
 | 
				
			||||||
            var data = _tds[columnId];
 | 
					            var data = _tds[columnId];
 | 
				
			||||||
            return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
 | 
					            return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public async Task ShowColumnOptionsDialog()
 | 
					        public async Task ShowColumnOptionsDialog()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths);
 | 
					            var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths, _columnOrder);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (result == default)
 | 
					            if (result == default)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -292,18 +393,27 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
                SelectedColumns = result.SelectedColumns;
 | 
					                SelectedColumns = result.SelectedColumns;
 | 
				
			||||||
                await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
 | 
					                await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
 | 
				
			||||||
                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
					                await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
 | 
				
			||||||
 | 
					                MarkColumnsDirty();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
 | 
					            if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                _columnWidths = result.ColumnWidths;
 | 
					                _columnWidths = result.ColumnWidths;
 | 
				
			||||||
                await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
 | 
					                await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
 | 
				
			||||||
 | 
					                MarkColumnsDirty();
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                _columnOrder = result.ColumnOrder;
 | 
				
			||||||
 | 
					                await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
 | 
				
			||||||
 | 
					                MarkColumnsDirty();
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static bool DictionaryEqual(Dictionary<string, int?> left, Dictionary<string, int?> right)
 | 
					        private static bool DictionaryEqual<TKey, TValue>(Dictionary<TKey, TValue> left, Dictionary<TKey, TValue> right) where TKey : notnull
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && left[k] == right[k]);
 | 
					            return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && Equals(left[k], right[k]));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private static string? GetColumnStyle(ColumnDefinition<T> column)
 | 
					        private static string? GetColumnStyle(ColumnDefinition<T> column)
 | 
				
			||||||
@@ -338,17 +448,34 @@ namespace Lantean.QBTMud.Components.UI
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            if (column.Width.HasValue)
 | 
					            if (column.Width.HasValue)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                className = $"overflow-cell {className}";
 | 
					                className = string.IsNullOrWhiteSpace(className)
 | 
				
			||||||
 | 
					                    ? "overflow-cell"
 | 
				
			||||||
 | 
					                    : $"overflow-cell {className}";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (OnTableDataContextMenu.HasDelegate)
 | 
					            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;
 | 
					            return className;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private void MarkColumnsDirty()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            _columnsDirty = true;
 | 
				
			||||||
 | 
					            _visibleColumns = EmptyColumns;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        private sealed record SortData
 | 
					        private sealed record SortData
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            public SortData(string sortColumn, SortDirection sortDirection)
 | 
					            public SortData(string sortColumn, SortDirection sortDirection)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
@inherits MudTd
 | 
					@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
 | 
					    @ChildContent
 | 
				
			||||||
</td>
 | 
					</td>
 | 
				
			||||||
@@ -1,6 +1,10 @@
 | 
				
			|||||||
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
 | 
					        <DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
 | 
				
			||||||
                      ColumnDefinitions="Columns"
 | 
					                      ColumnDefinitions="Columns"
 | 
				
			||||||
                      Items="WebSeeds"
 | 
					                      Items="WebSeeds"
 | 
				
			||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
              Class="details-list" />
 | 
					                      Class="details-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -328,13 +328,14 @@ namespace Lantean.QBTMud.Helpers
 | 
				
			|||||||
            return tags;
 | 
					            return tags;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths)
 | 
					        public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths, Dictionary<string, int> ColumnOrder)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths, Dictionary<string, int> order)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var parameters = new DialogParameters
 | 
					            var parameters = new DialogParameters
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                { nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
 | 
					                { nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
 | 
				
			||||||
                { nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
 | 
					                { nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
 | 
				
			||||||
                { nameof(ColumnOptionsDialog<T>.Widths), widths },
 | 
					                { nameof(ColumnOptionsDialog<T>.Widths), widths },
 | 
				
			||||||
 | 
					                { nameof(ColumnOptionsDialog<T>.Order), order },
 | 
				
			||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
 | 
					            var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
 | 
				
			||||||
@@ -344,7 +345,7 @@ namespace Lantean.QBTMud.Helpers
 | 
				
			|||||||
                return default;
 | 
					                return default;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return ((HashSet<string>, Dictionary<string, int?>))dialogResult.Data;
 | 
					            return ((HashSet<string>, Dictionary<string, int?>, Dictionary<string, int>))dialogResult.Data;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public static async Task<bool> ShowConfirmDialog(this IDialogService dialogService, string title, string content)
 | 
					        public static async Task<bool> ShowConfirmDialog(this IDialogService dialogService, string title, string content)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
 | 
				
			|||||||
        {
 | 
					        {
 | 
				
			||||||
            if (seconds is null)
 | 
					            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 "∞";
 | 
					                return "∞";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (seconds < 60)
 | 
					            if (value <= 0)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return "< 1m";
 | 
					                return "< 1m";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            TimeSpan time;
 | 
					            var time = TimeSpan.FromSeconds(value);
 | 
				
			||||||
            try
 | 
					            if (time.TotalMinutes < 1)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                time = TimeSpan.FromSeconds(seconds.Value);
 | 
					                return "< 1m";
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            catch (OverflowException)
 | 
					 | 
				
			||||||
            {
 | 
					 | 
				
			||||||
                return "∞";
 | 
					 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var sb = new StringBuilder();
 | 
					            var sb = new StringBuilder();
 | 
				
			||||||
            if (prefix is not null)
 | 
					            if (prefix is not null)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -129,7 +129,7 @@ namespace Lantean.QBTMud.Helpers
 | 
				
			|||||||
                return "";
 | 
					                return "";
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Size(size);
 | 
					            return Size(size, prefix, suffix);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        /// <summary>
 | 
					        /// <summary>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										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)
 | 
					            switch (category)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                case CATEGORY_ALL:
 | 
					                case CATEGORY_ALL:
 | 
				
			||||||
                    break;
 | 
					                    return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                case CATEGORY_UNCATEGORIZED:
 | 
					                case CATEGORY_UNCATEGORIZED:
 | 
				
			||||||
                    if (!string.IsNullOrEmpty(torrent.Category))
 | 
					                    if (!string.IsNullOrEmpty(torrent.Category))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        return false;
 | 
					                        return false;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                default:
 | 
					 | 
				
			||||||
                    if (!useSubcategories)
 | 
					 | 
				
			||||||
                    {
 | 
					 | 
				
			||||||
                        if (torrent.Category != category)
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            return false;
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                        else
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                            if (!torrent.Category.StartsWith(category))
 | 
					 | 
				
			||||||
                            {
 | 
					 | 
				
			||||||
                                return false;
 | 
					 | 
				
			||||||
                            }
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                    }
 | 
					 | 
				
			||||||
                    break;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    return true;
 | 
					                    return true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                default:
 | 
				
			||||||
 | 
					                    if (string.IsNullOrEmpty(torrent.Category))
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        return false;
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    if (!useSubcategories)
 | 
				
			||||||
 | 
					                    {
 | 
				
			||||||
 | 
					                        return string.Equals(torrent.Category, category, StringComparison.Ordinal);
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                    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)
 | 
					        public static bool FilterTag(Torrent torrent, string tag)
 | 
				
			||||||
@@ -207,7 +208,7 @@ namespace Lantean.QBTMud.Helpers
 | 
				
			|||||||
                    break;
 | 
					                    break;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                case Status.Paused:
 | 
					                case Status.Paused:
 | 
				
			||||||
                    if (!state.Contains("paused") || !state.Contains("stopped"))
 | 
					                    if (!state.Contains("paused") && !state.Contains("stopped"))
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        return false;
 | 
					                        return false;
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										33
									
								
								Lantean.QBTMud/Helpers/VersionHelper.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								Lantean.QBTMud/Helpers/VersionHelper.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,33 @@
 | 
				
			|||||||
 | 
					namespace Lantean.QBTMud.Helpers
 | 
				
			||||||
 | 
					{
 | 
				
			||||||
 | 
					    internal static class VersionHelper
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        private static int? _version;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        private const int _defaultVersion = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static int DefaultVersion => _defaultVersion;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        public static int GetMajorVersion(string? version)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (_version is not null)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return _version.Value;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (string.IsNullOrEmpty(version))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return _defaultVersion;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return _defaultVersion;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            _version = theVersion.Major;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return _version.Value;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
 | 
					<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <TargetFramework>net8.0</TargetFramework>
 | 
					    <TargetFramework>net9.0</TargetFramework>
 | 
				
			||||||
    <Nullable>enable</Nullable>
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
					    <ImplicitUsings>enable</ImplicitUsings>
 | 
				
			||||||
	  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
						  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
				
			||||||
@@ -12,13 +12,11 @@
 | 
				
			|||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
	  <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
 | 
						  <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
 | 
				
			||||||
	  <PackageReference Include="ByteSize" Version="2.1.2" />
 | 
						  <PackageReference Include="ByteSize" Version="2.1.2" />
 | 
				
			||||||
	<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
 | 
						  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
 | 
				
			||||||
	<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
 | 
						  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
 | 
				
			||||||
	<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
 | 
						  <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
 | 
				
			||||||
	<PackageReference Include="MudBlazor" Version="7.15.0" />
 | 
						  <PackageReference Include="MudBlazor" Version="8.13.0" />
 | 
				
			||||||
	<PackageReference Include="MudBlazor.ThemeManager" Version="2.1.0" />
 | 
						  <PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
 | 
				
			||||||
    <!-- added to fix vuln in dependency -->
 | 
					 | 
				
			||||||
	<PackageReference Include="System.Text.Json" Version="8.0.5" />
 | 
					 | 
				
			||||||
  </ItemGroup>
 | 
					  </ItemGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <ItemGroup>
 | 
					  <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,11 @@
 | 
				
			|||||||
@inherits LayoutComponentBase
 | 
					@inherits LayoutComponentBase
 | 
				
			||||||
@layout LoggedInLayout
 | 
					@layout LoggedInLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
					<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" />
 | 
					        <TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
 | 
				
			||||||
    </MudDrawer>
 | 
					    </MudDrawer>
 | 
				
			||||||
<MudMainContent>
 | 
					    <MudMainContent Class="app-shell__main">
 | 
				
			||||||
        @Body
 | 
					        @Body
 | 
				
			||||||
    </MudMainContent>
 | 
					    </MudMainContent>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
@inherits LayoutComponentBase
 | 
					@inherits LayoutComponentBase
 | 
				
			||||||
@layout LoggedInLayout
 | 
					@layout LoggedInLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
					<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" />
 | 
					        <FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
 | 
				
			||||||
    </MudDrawer>
 | 
					    </MudDrawer>
 | 
				
			||||||
<MudMainContent>
 | 
					    <MudMainContent Class="app-shell__main">
 | 
				
			||||||
        <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
					        <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
				
			||||||
            @Body
 | 
					            @Body
 | 
				
			||||||
        </CascadingValue>
 | 
					        </CascadingValue>
 | 
				
			||||||
    </MudMainContent>
 | 
					    </MudMainContent>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -10,6 +10,7 @@
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<CascadingValue Value="Torrents">
 | 
					<CascadingValue Value="Torrents">
 | 
				
			||||||
 | 
					    <CascadingValue Value="_torrentsVersion" Name="TorrentsVersion">
 | 
				
			||||||
        <CascadingValue Value="MainData">
 | 
					        <CascadingValue Value="MainData">
 | 
				
			||||||
            <CascadingValue Value="Preferences">
 | 
					            <CascadingValue Value="Preferences">
 | 
				
			||||||
                <CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
 | 
					                <CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
 | 
				
			||||||
@@ -23,36 +24,25 @@
 | 
				
			|||||||
                                                <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
					                                                <CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
 | 
				
			||||||
                                                    <CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
 | 
					                                                    <CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
 | 
				
			||||||
                                                        <CascadingValue Value="Version" Name="Version">
 | 
					                                                        <CascadingValue Value="Version" Name="Version">
 | 
				
			||||||
 | 
					                                                            <div class="app-shell">
 | 
				
			||||||
                                                                @Body
 | 
					                                                                @Body
 | 
				
			||||||
                                                    </CascadingValue>
 | 
					                                                                <MudAppBar Bottom="true" Elevation="0" Dense="true" Class="app-shell__status-bar">
 | 
				
			||||||
                                                </CascadingValue>
 | 
					 | 
				
			||||||
                                            </CascadingValue>
 | 
					 | 
				
			||||||
                                        </CascadingValue>
 | 
					 | 
				
			||||||
                                    </CascadingValue>
 | 
					 | 
				
			||||||
                                </CascadingValue>
 | 
					 | 
				
			||||||
                            </CascadingValue>
 | 
					 | 
				
			||||||
                        </CascadingValue>
 | 
					 | 
				
			||||||
                    </CascadingValue>
 | 
					 | 
				
			||||||
                </CascadingValue>
 | 
					 | 
				
			||||||
            </CascadingValue>
 | 
					 | 
				
			||||||
        </CascadingValue>
 | 
					 | 
				
			||||||
        <MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten);">
 | 
					 | 
				
			||||||
                                                                    @if (MainData?.LostConnection == true)
 | 
					                                                                    @if (MainData?.LostConnection == true)
 | 
				
			||||||
                                                                    {
 | 
					                                                                    {
 | 
				
			||||||
                <MudText Class="mx-2 mb-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
 | 
					                                                                        <MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
 | 
				
			||||||
                                                                    }
 | 
					                                                                    }
 | 
				
			||||||
                                                                    <MudSpacer />
 | 
					                                                                    <MudSpacer />
 | 
				
			||||||
            <MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
 | 
					                                                                    <MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					                                                                    <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
				
			||||||
            <MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
 | 
					                                                                    <MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					                                                                    <MudDivider Vertical="true" Class="d-none d-sm-flex" />
 | 
				
			||||||
                                                                    @{
 | 
					                                                                    @{
 | 
				
			||||||
                                                                        var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
 | 
					                                                                        var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
 | 
				
			||||||
                                                                    }
 | 
					                                                                    }
 | 
				
			||||||
            <MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
 | 
					                                                                    <MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="@MainData?.ServerState.ConnectionStatus" />
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					                                                                    <MudDivider Vertical="true" Class="" />
 | 
				
			||||||
                                                                    <MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
 | 
					                                                                    <MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
 | 
				
			||||||
            <MudDivider Vertical="true" />
 | 
					                                                                    <MudDivider Vertical="true" Class="" />
 | 
				
			||||||
                                                                    <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
 | 
					                                                                    <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
 | 
				
			||||||
                                                                    <MudText Class="mr-1 mb-1">
 | 
					                                                                    <MudText Class="mr-1 mb-1">
 | 
				
			||||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
 | 
					                                                                        @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
 | 
				
			||||||
@@ -65,5 +55,19 @@
 | 
				
			|||||||
                                                                        @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
 | 
					                                                                        @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
 | 
				
			||||||
                                                                    </MudText>
 | 
					                                                                    </MudText>
 | 
				
			||||||
                                                                </MudAppBar>
 | 
					                                                                </MudAppBar>
 | 
				
			||||||
 | 
					                                                            </div>
 | 
				
			||||||
 | 
					                                                        </CascadingValue>
 | 
				
			||||||
 | 
					                                                    </CascadingValue>
 | 
				
			||||||
 | 
					                                                </CascadingValue>
 | 
				
			||||||
 | 
					                                            </CascadingValue>
 | 
				
			||||||
 | 
					                                        </CascadingValue>
 | 
				
			||||||
 | 
					                                    </CascadingValue>
 | 
				
			||||||
 | 
					                                </CascadingValue>
 | 
				
			||||||
 | 
					                            </CascadingValue>
 | 
				
			||||||
 | 
					                        </CascadingValue>
 | 
				
			||||||
 | 
					                    </CascadingValue>
 | 
				
			||||||
 | 
					                </CascadingValue>
 | 
				
			||||||
 | 
					            </CascadingValue>
 | 
				
			||||||
 | 
					        </CascadingValue>
 | 
				
			||||||
    </CascadingValue>
 | 
					    </CascadingValue>
 | 
				
			||||||
</CascadingValue>
 | 
					</CascadingValue>
 | 
				
			||||||
@@ -52,22 +52,36 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected string? SearchText { get; set; }
 | 
					        protected string? SearchText { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected IEnumerable<Torrent> Torrents => GetTorrents();
 | 
					        protected IReadOnlyList<Torrent> Torrents => GetTorrents();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected bool IsAuthenticated { get; set; }
 | 
					        protected bool IsAuthenticated { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected bool LostConnection { 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)
 | 
					            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);
 | 
					            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()
 | 
					        protected override async Task OnInitializedAsync()
 | 
				
			||||||
@@ -83,7 +97,8 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
            Preferences = await ApiClient.GetApplicationPreferences();
 | 
					            Preferences = await ApiClient.GetApplicationPreferences();
 | 
				
			||||||
            Version = await ApiClient.GetApplicationVersion();
 | 
					            Version = await ApiClient.GetApplicationVersion();
 | 
				
			||||||
            var data = await ApiClient.GetMainData(_requestId);
 | 
					            var data = await ApiClient.GetMainData(_requestId);
 | 
				
			||||||
            MainData = DataManager.CreateMainData(data);
 | 
					            MainData = DataManager.CreateMainData(data, Version);
 | 
				
			||||||
 | 
					            MarkTorrentsDirty();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            _requestId = data.ResponseId;
 | 
					            _requestId = data.ResponseId;
 | 
				
			||||||
            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
					            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
				
			||||||
@@ -126,32 +141,51 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
                            return;
 | 
					                            return;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        var shouldRender = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if (MainData is null || data.FullUpdate)
 | 
					                        if (MainData is null || data.FullUpdate)
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            MainData = DataManager.CreateMainData(data);
 | 
					                            MainData = DataManager.CreateMainData(data, Version);
 | 
				
			||||||
 | 
					                            MarkTorrentsDirty();
 | 
				
			||||||
 | 
					                            shouldRender = true;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
                        else
 | 
					                        else
 | 
				
			||||||
                        {
 | 
					                        {
 | 
				
			||||||
                            DataManager.MergeMainData(data, MainData);
 | 
					                            var dataChanged = DataManager.MergeMainData(data, MainData, out var filterChanged);
 | 
				
			||||||
 | 
					                            if (filterChanged)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                MarkTorrentsDirty();
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            else if (dataChanged)
 | 
				
			||||||
 | 
					                            {
 | 
				
			||||||
 | 
					                                IncrementTorrentsVersion();
 | 
				
			||||||
 | 
					                            }
 | 
				
			||||||
 | 
					                            shouldRender = dataChanged;
 | 
				
			||||||
                        }
 | 
					                        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        if (MainData is not null)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
                            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
					                            _refreshInterval = MainData.ServerState.RefreshInterval;
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
                        _requestId = data.ResponseId;
 | 
					                        _requestId = data.ResponseId;
 | 
				
			||||||
 | 
					                        if (shouldRender)
 | 
				
			||||||
 | 
					                        {
 | 
				
			||||||
                            await InvokeAsync(StateHasChanged);
 | 
					                            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);
 | 
					        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)
 | 
					        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),
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
 | 
					        private void OnCategoryChanged(string category)
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            if (Category == category)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                return;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            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)
 | 
					        protected virtual void Dispose(bool disposing)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,19 +20,21 @@
 | 
				
			|||||||
                        <MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
 | 
					                        <MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
 | 
				
			||||||
                    </MudBadge>
 | 
					                    </MudBadge>
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                <MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
 | 
					                <MudSwitch T="bool" Label="Dark Mode" LabelPlacement="Placement.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
 | 
				
			||||||
                <Menu @ref="Menu" />
 | 
					                <Menu @ref="Menu" />
 | 
				
			||||||
            </MudAppBar>
 | 
					            </MudAppBar>
 | 
				
			||||||
            <MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
 | 
					            <MudDrawer @bind-Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
 | 
				
			||||||
                <ErrorDisplay ErrorBoundary="ErrorBoundary" />
 | 
					                <ErrorDisplay ErrorBoundary="ErrorBoundary" />
 | 
				
			||||||
            </MudDrawer>
 | 
					            </MudDrawer>
 | 
				
			||||||
            <CascadingValue Value="Theme">
 | 
					            <CascadingValue Value="Theme">
 | 
				
			||||||
                <CascadingValue Value="IsDarkMode" Name="IsDarkMode">
 | 
					                <CascadingValue Value="IsDarkMode" Name="IsDarkMode">
 | 
				
			||||||
                    <CascadingValue Value="Menu">
 | 
					                    <CascadingValue Value="Menu">
 | 
				
			||||||
 | 
					                        <CascadingValue Value="DrawerOpen" Name="DrawerOpen">
 | 
				
			||||||
                            @Body
 | 
					                            @Body
 | 
				
			||||||
                        </CascadingValue>
 | 
					                        </CascadingValue>
 | 
				
			||||||
                    </CascadingValue>
 | 
					                    </CascadingValue>
 | 
				
			||||||
                </CascadingValue>
 | 
					                </CascadingValue>
 | 
				
			||||||
 | 
					            </CascadingValue>
 | 
				
			||||||
        </MudLayout>
 | 
					        </MudLayout>
 | 
				
			||||||
    </EnhancedErrorBoundary>
 | 
					    </EnhancedErrorBoundary>
 | 
				
			||||||
</CascadingValue>
 | 
					</CascadingValue>
 | 
				
			||||||
@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        private bool _disposedValue;
 | 
					        private bool _disposedValue;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Inject]
 | 
					 | 
				
			||||||
        protected NavigationManager NavigationManager { get; set; } = default!;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        [Inject]
 | 
					        [Inject]
 | 
				
			||||||
        private IBrowserViewportService BrowserViewportService { get; set; } = default!;
 | 
					        private IBrowserViewportService BrowserViewportService { get; set; } = default!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -78,21 +75,21 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    IsDarkMode = isDarkMode.Value;
 | 
					                    IsDarkMode = isDarkMode.Value;
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
 | 
					                await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
 | 
				
			||||||
                await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
 | 
					                await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
 | 
				
			||||||
                await InvokeAsync(StateHasChanged);
 | 
					                await InvokeAsync(StateHasChanged);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected Task OnSystemPreferenceChanged(bool value)
 | 
					        protected Task OnSystemDarkModeChanged(bool value)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            IsDarkMode = value;
 | 
					            IsDarkMode = value;
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            return Task.CompletedTask;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
 | 
					        public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
 | 
					            if (browserViewportEventArgs.Breakpoint <= Breakpoint.Sm)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                DrawerOpen = false;
 | 
					                DrawerOpen = false;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -101,7 +98,17 @@ namespace Lantean.QBTMud.Layout
 | 
				
			|||||||
                DrawerOpen = true;
 | 
					                DrawerOpen = true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            return Task.CompletedTask;
 | 
					            if (ErrorBoundary?.Errors.Count > 0)
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                ErrorDrawerOpen = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            else
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                await Task.Delay(250);
 | 
				
			||||||
 | 
					                ErrorDrawerOpen = false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            await InvokeAsync(StateHasChanged);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected void ToggleErrorDrawer()
 | 
					        protected void ToggleErrorDrawer()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,13 @@
 | 
				
			|||||||
@inherits LayoutComponentBase
 | 
					@inherits LayoutComponentBase
 | 
				
			||||||
@layout LoggedInLayout
 | 
					@layout LoggedInLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
 | 
					<div class="app-shell__body">
 | 
				
			||||||
 | 
					    <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
 | 
				
			||||||
        <MudNavMenu>
 | 
					        <MudNavMenu>
 | 
				
			||||||
            <ApplicationActions IsMenu="false" Preferences="Preferences" />
 | 
					            <ApplicationActions IsMenu="false" Preferences="Preferences" />
 | 
				
			||||||
        </MudNavMenu>
 | 
					        </MudNavMenu>
 | 
				
			||||||
    </MudDrawer>
 | 
					    </MudDrawer>
 | 
				
			||||||
<MudMainContent>
 | 
					    <MudMainContent Class="app-shell__main">
 | 
				
			||||||
        @Body
 | 
					        @Body
 | 
				
			||||||
    </MudMainContent>
 | 
					    </MudMainContent>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,8 @@
 | 
				
			|||||||
            Dictionary<string, HashSet<string>> tagState,
 | 
					            Dictionary<string, HashSet<string>> tagState,
 | 
				
			||||||
            Dictionary<string, HashSet<string>> categoriesState,
 | 
					            Dictionary<string, HashSet<string>> categoriesState,
 | 
				
			||||||
            Dictionary<string, HashSet<string>> statusState,
 | 
					            Dictionary<string, HashSet<string>> statusState,
 | 
				
			||||||
            Dictionary<string, HashSet<string>> trackersState)
 | 
					            Dictionary<string, HashSet<string>> trackersState,
 | 
				
			||||||
 | 
					            int majorVersion)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            Torrents = torrents.ToDictionary();
 | 
					            Torrents = torrents.ToDictionary();
 | 
				
			||||||
            Tags = tags.ToHashSet();
 | 
					            Tags = tags.ToHashSet();
 | 
				
			||||||
@@ -22,6 +23,7 @@
 | 
				
			|||||||
            CategoriesState = categoriesState;
 | 
					            CategoriesState = categoriesState;
 | 
				
			||||||
            StatusState = statusState;
 | 
					            StatusState = statusState;
 | 
				
			||||||
            TrackersState = trackersState;
 | 
					            TrackersState = trackersState;
 | 
				
			||||||
 | 
					            MajorVersion = majorVersion;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public Dictionary<string, Torrent> Torrents { get; }
 | 
					        public Dictionary<string, Torrent> Torrents { get; }
 | 
				
			||||||
@@ -36,5 +38,6 @@
 | 
				
			|||||||
        public Dictionary<string, HashSet<string>> TrackersState { get; }
 | 
					        public Dictionary<string, HashSet<string>> TrackersState { get; }
 | 
				
			||||||
        public string? SelectedTorrentHash { get; set; }
 | 
					        public string? SelectedTorrentHash { get; set; }
 | 
				
			||||||
        public bool LostConnection { get; set; }
 | 
					        public bool LostConnection { get; set; }
 | 
				
			||||||
 | 
					        public int MajorVersion { get; }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -8,6 +8,7 @@
 | 
				
			|||||||
        Completed,
 | 
					        Completed,
 | 
				
			||||||
        Resumed,
 | 
					        Resumed,
 | 
				
			||||||
        Paused,
 | 
					        Paused,
 | 
				
			||||||
 | 
					        Stopped,
 | 
				
			||||||
        Active,
 | 
					        Active,
 | 
				
			||||||
        Inactive,
 | 
					        Inactive,
 | 
				
			||||||
        Stalled,
 | 
					        Stalled,
 | 
				
			||||||
@@ -15,6 +16,5 @@
 | 
				
			|||||||
        StalledDownloading,
 | 
					        StalledDownloading,
 | 
				
			||||||
        Checking,
 | 
					        Checking,
 | 
				
			||||||
        Errored,
 | 
					        Errored,
 | 
				
			||||||
        Stopped
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,4 @@
 | 
				
			|||||||
using Lantean.QBitTorrentClient.Models;
 | 
					namespace Lantean.QBTMud.Models
 | 
				
			||||||
 | 
					 | 
				
			||||||
namespace Lantean.QBTMud.Models
 | 
					 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public record TorrentOptions
 | 
					    public record TorrentOptions
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/about"
 | 
					@page "/about"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -9,99 +11,108 @@
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            <MudText Class="px-5 no-wrap">About</MudText>
 | 
					            <MudText Class="px-5 no-wrap">About</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <MudTabs Elevation="2" ApplyEffectsToContainer="true">
 | 
					        <MudTabs Elevation="2" ApplyEffectsToContainer="true">
 | 
				
			||||||
            <MudTabPanel Text="About">
 | 
					            <MudTabPanel Text="About">
 | 
				
			||||||
        <div class="d-flex gap-4">
 | 
					                <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 content-panel__container options-tab-contents">
 | 
				
			||||||
            <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop" Height="162" Width="94" />
 | 
					            <MudGrid Class="mt-0 mb-4">
 | 
				
			||||||
            <MudGrid Class="mx-0 mt-0 mb-3">
 | 
					                <MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
 | 
				
			||||||
                <MudItem xs="12">
 | 
					                    <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
 | 
				
			||||||
                    <div class="d-flex gap-3">
 | 
					                              Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop"
 | 
				
			||||||
                        <MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None" Alt="QBT" Height="32" Width="32"  /><MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
 | 
					                              Height="162" Width="94" />
 | 
				
			||||||
 | 
					                </MudItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                <MudItem xs="12" sm="9" md="10" lg="10" xl="11">
 | 
				
			||||||
 | 
					                    <div class="d-flex flex-column gap-2">
 | 
				
			||||||
 | 
					                        <div class="d-flex gap-3 align-items-center">
 | 
				
			||||||
 | 
					                            <MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None"
 | 
				
			||||||
 | 
					                                      Alt="QBT" Height="32" Width="32" />
 | 
				
			||||||
 | 
					                            <MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
 | 
				
			||||||
                        </div>
 | 
					                        </div>
 | 
				
			||||||
                </MudItem>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="12">
 | 
					                        <MudText Typo="Typo.body1">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.</MudText>
 | 
					                            An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.
 | 
				
			||||||
                </MudItem>
 | 
					                        </MudText>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="12">
 | 
					 | 
				
			||||||
                        <MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
 | 
					                        <MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                        <div class="d-flex flex-wrap">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Home Page</MudText>
 | 
					                            <MudText Typo="Typo.body1" Class="fw-bold">Home Page: </MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                            <MudLink Href="https://www.qbittorrent.org" Target="_blank" Class="ms-2">
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                                qbittorrent.org
 | 
				
			||||||
                    <MudLink Href="https://www.qbittorrent.org" Target="https://www.qbittorrent.org">https://www.qbittorrent.org</MudLink>
 | 
					                            </MudLink>
 | 
				
			||||||
                </MudItem>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                        <div class="d-flex flex-wrap">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Bug Tracker</MudText>
 | 
					                            <MudText Typo="Typo.body1" Class="fw-bold">Bug Tracker: </MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                            <MudLink Href="https://bugs.qbittorrent.org" Target="_blank" Class="ms-2">
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                                bugs.qbittorrent.org
 | 
				
			||||||
                    <MudLink Href="https://bugs.qbittorrent.org" Target="https://bugs.qbittorrent.org">https://bugs.qbittorrent.org</MudLink>
 | 
					                            </MudLink>
 | 
				
			||||||
                </MudItem>
 | 
					                        </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                        <div class="d-flex flex-wrap">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Forum</MudText>
 | 
					                            <MudText Typo="Typo.body1" Class="fw-bold">Forum: </MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                            <MudLink Href="https://forum.qbittorrent.org" Target="_blank" Class="ms-2">
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                                forum.qbittorrent.org
 | 
				
			||||||
                    <MudLink Href="https://forum.qbittorrent.org" Target="https://forum.qbittorrent.org">https://forum.qbittorrent.org</MudLink>
 | 
					                            </MudLink>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                    </div>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
        </div>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
    <MudTabPanel Text="Authors">
 | 
					    <MudTabPanel Text="Authors">
 | 
				
			||||||
        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
 | 
					        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
 | 
				
			||||||
            <MudText Typo="Typo.body1" Class="py-1">Current maintainer</MudText>
 | 
					            <MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <MudGrid Class="mt-0 mb-4">
 | 
					            <MudGrid Class="mt-0 mb-4">
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Name</MudText>
 | 
					                    <MudText Typo="Typo.h6">Name</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Sledgehammer999</MudText>
 | 
					                    <MudText Typo="Typo.body1">Sledgehammer999</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Nationality</MudText>
 | 
					                    <MudText Typo="Typo.h6">Nationality</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Greece</MudText>
 | 
					                    <MudText Typo="Typo.body1">Greece</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">E-mail</MudText>
 | 
					                    <MudText Typo="Typo.h6">E-mail</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
 | 
					                    <MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <MudText Typo="Typo.body1" Class="py-1">Original author</MudText>
 | 
					            <MudText Typo="Typo.h5" Class="py-1">Original author</MudText>
 | 
				
			||||||
            <MudGrid Class="mt-0 mb-4">
 | 
					            <MudGrid Class="mt-0 mb-4">
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Name</MudText>
 | 
					                    <MudText Typo="Typo.h6">Name</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Christophe Dumez</MudText>
 | 
					                    <MudText Typo="Typo.body1">Christophe Dumez</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Nationality</MudText>
 | 
					                    <MudText Typo="Typo.h6">Nationality</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">France</MudText>
 | 
					                    <MudText Typo="Typo.body1">France</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="12" md="2">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">E-mail</MudText>
 | 
					                    <MudText Typo="Typo.h6">E-mail</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="12" md="10">
 | 
				
			||||||
                    <MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
 | 
					                    <MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
    <MudTabPanel Text="Special Thanks">
 | 
					    <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 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 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>
 | 
					            <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>
 | 
				
			||||||
@@ -111,14 +122,14 @@
 | 
				
			|||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
    <MudTabPanel Text="Translators">
 | 
					    <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">
 | 
					            <MudText Typo="Typo.body1" Class="py-1">
 | 
				
			||||||
                I would like to thank the people who volunteered to Circle qBittorrent.<br>
 | 
					                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>
 | 
					                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>
 | 
				
			||||||
                (the list might not be up to date)
 | 
					                (the list might not be up to date)
 | 
				
			||||||
            </MudText>
 | 
					            </MudText>
 | 
				
			||||||
            <MudList T="string" ReadOnly>
 | 
					            <MudList T="string" ReadOnly>
 | 
				
			||||||
                <MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Info"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
 | 
					                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
 | 
				
			||||||
                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Armenian:</u> Hrant Ohanyan (hrantohanyan@mail.am)</MudListItem>
 | 
					                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Armenian:</u> Hrant Ohanyan (hrantohanyan@mail.am)</MudListItem>
 | 
				
			||||||
                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
 | 
					                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
 | 
				
			||||||
                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@gmail.com)</MudListItem>
 | 
					                <MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@gmail.com)</MudListItem>
 | 
				
			||||||
@@ -161,7 +172,7 @@
 | 
				
			|||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
    <MudTabPanel Text="Licence">
 | 
					    <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">
 | 
					            <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+).
 | 
					                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+),
 | 
					                However, this binary distribution is licensed under GNU General Public License, version 3 or (at your option) any later version (GPLv3+),
 | 
				
			||||||
@@ -1054,42 +1065,42 @@
 | 
				
			|||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
    <MudTabPanel Text="Software Used">
 | 
					    <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>
 | 
					            <MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            <MudGrid Class="mt-1 mb-4">
 | 
					            <MudGrid Class="mt-1 mb-4">
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="3">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Qt</MudText>
 | 
					                    <MudText Typo="Typo.body1">Qt</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="9">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">@QtVersion</MudText>
 | 
					                    <MudText Typo="Typo.body1">@QtVersion</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="3">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Libtorrent</MudText>
 | 
					                    <MudText Typo="Typo.body1">Libtorrent</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="9">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
 | 
					                    <MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="3">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">Boost</MudText>
 | 
					                    <MudText Typo="Typo.body1">Boost</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="9">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">@BoostVersion</MudText>
 | 
					                    <MudText Typo="Typo.body1">@BoostVersion</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="3">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">OpenSSL</MudText>
 | 
					                    <MudText Typo="Typo.body1">OpenSSL</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="9">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">@OpensslVersion</MudText>
 | 
					                    <MudText Typo="Typo.body1">@OpensslVersion</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                <MudItem xs="2">
 | 
					                <MudItem xs="3">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">zlib</MudText>
 | 
					                    <MudText Typo="Typo.body1">zlib</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
                <MudItem xs="10">
 | 
					                <MudItem xs="9">
 | 
				
			||||||
                    <MudText Typo="Typo.body1">@ZlibVersion</MudText>
 | 
					                    <MudText Typo="Typo.body1">@ZlibVersion</MudText>
 | 
				
			||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
@@ -1098,3 +1109,5 @@
 | 
				
			|||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
    </MudTabPanel>
 | 
					    </MudTabPanel>
 | 
				
			||||||
</MudTabs>
 | 
					</MudTabs>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/blocks"
 | 
					@page "/blocks"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -10,7 +12,8 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
 | 
					            <MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
					        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
				
			||||||
            <MudCardContent>
 | 
					            <MudCardContent>
 | 
				
			||||||
                <EditForm Model="Model" OnSubmit="Submit">
 | 
					                <EditForm Model="Model" OnSubmit="Submit">
 | 
				
			||||||
@@ -33,4 +36,6 @@
 | 
				
			|||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
                      RowClassFunc="RowClass"
 | 
					                      RowClassFunc="RowClass"
 | 
				
			||||||
              Class="search-list" />
 | 
					                      Class="search-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/categories"
 | 
					@page "/categories"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -11,14 +13,18 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
 | 
					            <MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <DynamicTable @ref="Table"
 | 
					        <DynamicTable @ref="Table"
 | 
				
			||||||
                      T="Category"
 | 
					                      T="Category"
 | 
				
			||||||
                      ColumnDefinitions="Columns"
 | 
					                      ColumnDefinitions="Columns"
 | 
				
			||||||
                      Items="Results"
 | 
					                      Items="Results"
 | 
				
			||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
              Class="details-list" />
 | 
					                      Class="details-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@code {
 | 
					@code {
 | 
				
			||||||
    private RenderFragment<RowContext<Category>> ActionsColumn
 | 
					    private RenderFragment<RowContext<Category>> ActionsColumn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/details/{hash}"
 | 
					@page "/details/{hash}"
 | 
				
			||||||
@layout DetailsLayout
 | 
					@layout DetailsLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -14,7 +16,9 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudText Class="pl-5 no-wrap">@Name</MudText>
 | 
					            <MudText Class="pl-5 no-wrap">@Name</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        @if (ShowTabs)
 | 
					        @if (ShowTabs)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            <CascadingValue Value="RefreshInterval" Name="RefreshInterval">
 | 
					            <CascadingValue Value="RefreshInterval" Name="RefreshInterval">
 | 
				
			||||||
@@ -37,3 +41,5 @@
 | 
				
			|||||||
                </MudTabs>
 | 
					                </MudTabs>
 | 
				
			||||||
            </CascadingValue>
 | 
					            </CascadingValue>
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/log"
 | 
					@page "/log"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -10,7 +12,8 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudText Class="pl-5 no-wrap">Execution Log</MudText>
 | 
					            <MudText Class="pl-5 no-wrap">Execution Log</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
					        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
				
			||||||
            <MudCardContent>
 | 
					            <MudCardContent>
 | 
				
			||||||
                <EditForm Model="Model" OnSubmit="Submit">
 | 
					                <EditForm Model="Model" OnSubmit="Submit">
 | 
				
			||||||
@@ -41,4 +44,6 @@
 | 
				
			|||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
                      RowClassFunc="RowClass"
 | 
					                      RowClassFunc="RowClass"
 | 
				
			||||||
              Class="search-list" />
 | 
					                      Class="search-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -49,7 +49,7 @@ namespace Lantean.QBTMud.Pages
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected override Task OnInitializedAsync()
 | 
					        protected override Task OnInitializedAsync()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            return DoLogin("admin", "eBGJzbjkJ");
 | 
					            return DoLogin("admin", "5FUM5pATq");
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
#endif
 | 
					#endif
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
 | 
					<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -14,30 +16,50 @@
 | 
				
			|||||||
            <MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
					            <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)" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
 | 
					        <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
 | 
				
			||||||
            <MudTabPanel Text="Behaviour">
 | 
					            <MudTabPanel Text="Behaviour">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="Downloads">
 | 
					            <MudTabPanel Text="Downloads">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="Connection">
 | 
					            <MudTabPanel Text="Connection">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="Speed">
 | 
					            <MudTabPanel Text="Speed">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="BitTorrent">
 | 
					            <MudTabPanel Text="BitTorrent">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="RSS">
 | 
					            <MudTabPanel Text="RSS">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="Web UI">
 | 
					            <MudTabPanel Text="Web UI">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
            <MudTabPanel Text="Advanced">
 | 
					            <MudTabPanel Text="Advanced">
 | 
				
			||||||
 | 
					                <div class="options-tab-contents">
 | 
				
			||||||
                    <AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
					                    <AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
 | 
				
			||||||
 | 
					                </div>
 | 
				
			||||||
            </MudTabPanel>
 | 
					            </MudTabPanel>
 | 
				
			||||||
        </MudTabs>
 | 
					        </MudTabs>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/rss"
 | 
					@page "/rss"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -15,8 +17,10 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
 | 
					        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="content-panel__container">
 | 
				
			||||||
            <MudGrid Class="rss-contents">
 | 
					            <MudGrid Class="rss-contents">
 | 
				
			||||||
                <MudItem xs="4" Style="height: 100%">
 | 
					                <MudItem xs="4" Style="height: 100%">
 | 
				
			||||||
                    <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
 | 
					                    <MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
 | 
				
			||||||
@@ -71,3 +75,5 @@
 | 
				
			|||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/search"
 | 
					@page "/search"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -10,7 +12,8 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudText Class="pl-5 no-wrap">Search</MudText>
 | 
					            <MudText Class="pl-5 no-wrap">Search</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
					        <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
 | 
				
			||||||
            <MudCardContent>
 | 
					            <MudCardContent>
 | 
				
			||||||
                <EditForm Model="Model" OnValidSubmit="DoSearch">
 | 
					                <EditForm Model="Model" OnValidSubmit="DoSearch">
 | 
				
			||||||
@@ -18,7 +21,7 @@
 | 
				
			|||||||
                        <MudItem xs="12" md="4">
 | 
					                        <MudItem xs="12" md="4">
 | 
				
			||||||
                            <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
 | 
					                            <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
 | 
				
			||||||
                        </MudItem>
 | 
					                        </MudItem>
 | 
				
			||||||
                <MudItem xs="2" md="3">
 | 
					                        <MudItem xs="12" md="3">
 | 
				
			||||||
                            <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
 | 
					                            <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
 | 
				
			||||||
                                @foreach (var (value, name) in Categories)
 | 
					                                @foreach (var (value, name) in Categories)
 | 
				
			||||||
                                {
 | 
					                                {
 | 
				
			||||||
@@ -30,17 +33,21 @@
 | 
				
			|||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            </MudSelect>
 | 
					                            </MudSelect>
 | 
				
			||||||
                        </MudItem>
 | 
					                        </MudItem>
 | 
				
			||||||
                <MudItem xs="2" md="3">
 | 
					                        <MudItem xs="12" md="3">
 | 
				
			||||||
                            <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
 | 
					                            <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
 | 
				
			||||||
                                <MudSelectItem Value="@("all")">All</MudSelectItem>
 | 
					                                <MudSelectItem Value="@("all")">All</MudSelectItem>
 | 
				
			||||||
 | 
					                                @if (Plugins.Count > 0)
 | 
				
			||||||
 | 
					                                {
 | 
				
			||||||
                                    <MudDivider />
 | 
					                                    <MudDivider />
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                                }
 | 
				
			||||||
                                @foreach (var (value, name) in Plugins)
 | 
					                                @foreach (var (value, name) in Plugins)
 | 
				
			||||||
                                {
 | 
					                                {
 | 
				
			||||||
                                    <MudSelectItem Value="value">@name</MudSelectItem>
 | 
					                                    <MudSelectItem Value="value">@name</MudSelectItem>
 | 
				
			||||||
                                }
 | 
					                                }
 | 
				
			||||||
                            </MudSelect>
 | 
					                            </MudSelect>
 | 
				
			||||||
                        </MudItem>
 | 
					                        </MudItem>
 | 
				
			||||||
                <MudItem xs="2" md="2">
 | 
					                        <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>
 | 
					                            <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>
 | 
					                        </MudItem>
 | 
				
			||||||
                    
 | 
					                    
 | 
				
			||||||
@@ -55,4 +62,6 @@
 | 
				
			|||||||
                      Items="Results"
 | 
					                      Items="Results"
 | 
				
			||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
              Class="search-list" />
 | 
					                      Class="search-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/statistics"
 | 
					@page "/statistics"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -10,8 +12,10 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudText Class="pl-5 no-wrap">Statistics</MudText>
 | 
					            <MudText Class="pl-5 no-wrap">Statistics</MudText>
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents">
 | 
					    <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>
 | 
					            <MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
 | 
				
			||||||
            <MudGrid>
 | 
					            <MudGrid>
 | 
				
			||||||
                <MudItem xs="12">
 | 
					                <MudItem xs="12">
 | 
				
			||||||
@@ -60,3 +64,5 @@
 | 
				
			|||||||
                </MudItem>
 | 
					                </MudItem>
 | 
				
			||||||
            </MudGrid>
 | 
					            </MudGrid>
 | 
				
			||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
@page "/tags"
 | 
					@page "/tags"
 | 
				
			||||||
@layout OtherLayout
 | 
					@layout OtherLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            @if (!DrawerOpen)
 | 
					            @if (!DrawerOpen)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -11,14 +13,18 @@
 | 
				
			|||||||
            <MudDivider Vertical="true" />
 | 
					            <MudDivider Vertical="true" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
 | 
					            <MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
 | 
				
			||||||
        </MudToolBar>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
        <DynamicTable @ref="Table"
 | 
					        <DynamicTable @ref="Table"
 | 
				
			||||||
                      T="string"
 | 
					                      T="string"
 | 
				
			||||||
                      ColumnDefinitions="Columns"
 | 
					                      ColumnDefinitions="Columns"
 | 
				
			||||||
                      Items="Results"
 | 
					                      Items="Results"
 | 
				
			||||||
                      MultiSelection="false"
 | 
					                      MultiSelection="false"
 | 
				
			||||||
                      SelectOnRowClick="false"
 | 
					                      SelectOnRowClick="false"
 | 
				
			||||||
              Class="details-list" />
 | 
					                      Class="details-list content-panel__table" />
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@code {
 | 
					@code {
 | 
				
			||||||
    private RenderFragment<RowContext<string>> ActionsColumn
 | 
					    private RenderFragment<RowContext<string>> ActionsColumn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,12 +1,14 @@
 | 
				
			|||||||
@page "/"
 | 
					@page "/"
 | 
				
			||||||
@layout ListLayout
 | 
					@layout ListLayout
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<ContextMenu @ref="ContextMenu" Dense="true" AdjustmentX="@(DrawerOpen ? -235 : 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>
 | 
					    <MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
 | 
				
			||||||
    <MudDivider />
 | 
					    <MudDivider />
 | 
				
			||||||
    <TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
					    <TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
 | 
				
			||||||
</ContextMenu>
 | 
					</MudMenu>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<div class="content-panel">
 | 
				
			||||||
 | 
					    <div class="content-panel__toolbar content-panel__toolbar--scroll">
 | 
				
			||||||
        <MudToolBar Gutters="false" Dense="true">
 | 
					        <MudToolBar Gutters="false" Dense="true">
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
 | 
				
			||||||
            <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
 | 
					            <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
 | 
				
			||||||
@@ -18,12 +20,13 @@
 | 
				
			|||||||
            <MudSpacer />
 | 
					            <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>
 | 
					            <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>
 | 
					        </MudToolBar>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
 | 
					    <div class="content-panel__body">
 | 
				
			||||||
 | 
					        <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0 content-panel__container">
 | 
				
			||||||
            <DynamicTable
 | 
					            <DynamicTable
 | 
				
			||||||
                @ref="Table"
 | 
					                @ref="Table"
 | 
				
			||||||
                T="Torrent" 
 | 
					                T="Torrent" 
 | 
				
			||||||
        Class="torrent-list"
 | 
					                Class="torrent-list content-panel__table"
 | 
				
			||||||
                ColumnDefinitions="Columns" 
 | 
					                ColumnDefinitions="Columns" 
 | 
				
			||||||
                Items="Torrents" 
 | 
					                Items="Torrents" 
 | 
				
			||||||
                OnRowClick="RowClick" 
 | 
					                OnRowClick="RowClick" 
 | 
				
			||||||
@@ -36,6 +39,8 @@
 | 
				
			|||||||
                OnTableDataLongPress="TableDataLongPress"
 | 
					                OnTableDataLongPress="TableDataLongPress"
 | 
				
			||||||
            />
 | 
					            />
 | 
				
			||||||
        </MudContainer>
 | 
					        </MudContainer>
 | 
				
			||||||
 | 
					    </div>
 | 
				
			||||||
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@code {
 | 
					@code {
 | 
				
			||||||
    private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
 | 
					    private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,11 +35,17 @@ namespace Lantean.QBTMud.Pages
 | 
				
			|||||||
        public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
 | 
					        public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public IEnumerable<Torrent>? Torrents { get; set; }
 | 
					        public IReadOnlyList<Torrent>? Torrents { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [CascadingParameter]
 | 
					        [CascadingParameter]
 | 
				
			||||||
        public MainData MainData { get; set; } = default!;
 | 
					        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")]
 | 
					        [CascadingParameter(Name = "SearchTermChanged")]
 | 
				
			||||||
        public EventCallback<string> SearchTermChanged { get; set; }
 | 
					        public EventCallback<string> SearchTermChanged { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -56,13 +62,23 @@ namespace Lantean.QBTMud.Pages
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        protected HashSet<Torrent> SelectedItems { get; set; } = [];
 | 
					        protected HashSet<Torrent> SelectedItems { get; set; } = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
 | 
					        protected bool ToolbarButtonsEnabled => _toolbarButtonsEnabled;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected DynamicTable<Torrent>? Table { get; set; }
 | 
					        protected DynamicTable<Torrent>? Table { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected Torrent? ContextMenuItem { 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)
 | 
					        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)
 | 
					        protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            SelectedItems = selectedItems;
 | 
					            SelectedItems = selectedItems;
 | 
				
			||||||
 | 
					            _toolbarButtonsEnabled = SelectedItems.Count > 0;
 | 
				
			||||||
 | 
					            _pendingSelectionChange = true;
 | 
				
			||||||
 | 
					            InvokeAsync(StateHasChanged);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
 | 
					        protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
 | 
				
			||||||
@@ -185,7 +273,9 @@ namespace Lantean.QBTMud.Pages
 | 
				
			|||||||
                return;
 | 
					                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);
 | 
					        protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
 | 
				
			||||||
@@ -193,7 +283,7 @@ namespace Lantean.QBTMud.Pages
 | 
				
			|||||||
        public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
 | 
					        public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
 | 
				
			||||||
        [
 | 
					        [
 | 
				
			||||||
            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("#", t => t.Priority),
 | 
					            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("#", t => t.Priority),
 | 
				
			||||||
            ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25),
 | 
					            ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25, tdClass: "table-icon"),
 | 
				
			||||||
            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Name", t => t.Name, width: 400),
 | 
					            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Name", t => t.Name, width: 400),
 | 
				
			||||||
            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
 | 
					            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
 | 
				
			||||||
            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),
 | 
					            ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
using Blazored.LocalStorage;
 | 
					using Blazored.LocalStorage;
 | 
				
			||||||
using Lantean.QBitTorrentClient;
 | 
					using Lantean.QBitTorrentClient;
 | 
				
			||||||
using Lantean.QBTMud.Services;
 | 
					using Lantean.QBTMud.Services;
 | 
				
			||||||
using Microsoft.AspNetCore.Components.Web;
 | 
					using Microsoft.AspNetCore.Components.Web;
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -4,11 +4,11 @@ namespace Lantean.QBTMud.Services
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
    public interface IDataManager
 | 
					    public interface IDataManager
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
 | 
					        MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
 | 
					        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);
 | 
					        PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,7 +16,7 @@ namespace Lantean.QBTMud.Services
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
 | 
					        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);
 | 
					        QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,15 +65,11 @@ code {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.mud-appbar.mud-appbar-fixed-bottom {
 | 
					.mud-appbar.mud-appbar-fixed-bottom {
 | 
				
			||||||
    height: 35px;
 | 
					    height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
.mud-main-content {
 | 
					 | 
				
			||||||
    padding-bottom: 35px;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.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 {
 | 
					.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 {
 | 
					.w-100 {
 | 
				
			||||||
@@ -154,25 +150,91 @@ code {
 | 
				
			|||||||
    margin-right: 5px;
 | 
					    margin-right: 5px;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.torrent-list .mud-table-container {
 | 
					/*. Layout helpers */
 | 
				
			||||||
    height: calc(100vh - 149px);
 | 
					.content-panel {
 | 
				
			||||||
 | 
					    display: flex;
 | 
				
			||||||
 | 
					    flex-direction: column;
 | 
				
			||||||
 | 
					    height: 100%;
 | 
				
			||||||
 | 
					    min-height: 0;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.file-list .mud-table-container {
 | 
					.content-panel__toolbar {
 | 
				
			||||||
    height: calc(100vh - 245px);
 | 
					    flex: 0 0 auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.details-list .mud-table-container {
 | 
					.content-panel__toolbar--scroll {
 | 
				
			||||||
    height: calc(100vh - 200px);
 | 
					    overflow-x: auto;
 | 
				
			||||||
 | 
					    white-space: nowrap;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.details-tab-contents {
 | 
					.content-panel__body {
 | 
				
			||||||
    height: calc(100vh - 200px);
 | 
					    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;
 | 
					    overflow: auto;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.torrent-list .mud-table-container,
 | 
				
			||||||
 | 
					.file-list .mud-table-container,
 | 
				
			||||||
 | 
					.details-list .mud-table-container,
 | 
				
			||||||
.search-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 {
 | 
					tr.log-normal td {
 | 
				
			||||||
@@ -220,10 +282,6 @@ td .folder-button {
 | 
				
			|||||||
    padding: 6px 16px 6px 16px !important;
 | 
					    padding: 6px 16px 6px 16px !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
.rss-contents {
 | 
					 | 
				
			||||||
    height: calc(100vh - 149px);
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@keyframes spin {
 | 
					@keyframes spin {
 | 
				
			||||||
    0% {
 | 
					    0% {
 | 
				
			||||||
        transform: rotate(0deg);
 | 
					        transform: rotate(0deg);
 | 
				
			||||||
@@ -241,3 +299,131 @@ td .folder-button {
 | 
				
			|||||||
.mud-dialog .mud-dialog-content {
 | 
					.mud-dialog .mud-dialog-content {
 | 
				
			||||||
    padding-top: 4px !important;
 | 
					    padding-top: 4px !important;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.icon-menu-dense {
 | 
				
			||||||
 | 
					    padding-top: 2px;
 | 
				
			||||||
 | 
					    padding-bottom: 2px;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.table-icon {
 | 
				
			||||||
 | 
					    width: 25px;
 | 
				
			||||||
 | 
					    max-width: 25px;
 | 
				
			||||||
 | 
					    padding: 0 8px !important;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					.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;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,12 +9,12 @@
 | 
				
			|||||||
    <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
					    <link rel="preconnect" href="https://fonts.googleapis.com">
 | 
				
			||||||
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
					    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
 | 
				
			||||||
    <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
 | 
					    <link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
 | 
				
			||||||
    <link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
 | 
					    <link href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
 | 
				
			||||||
    <link rel="stylesheet" href="css/app.css" />
 | 
					    <link rel="stylesheet" href="./css/app.css" />
 | 
				
			||||||
    <link rel="icon" type="image/png" href="images/qbittorrent32.png" />
 | 
					    <link rel="icon" type="image/png" href="images/qbittorrent32.png" />
 | 
				
			||||||
    <link rel="icon" href="images/qbittorrent-tray.svg">
 | 
					    <link rel="icon" href="./images/qbittorrent-tray.svg">
 | 
				
			||||||
    <link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000">
 | 
					    <link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
 | 
				
			||||||
    <link rel="apple-touch-icon" href="images/qbittorrent32.png">
 | 
					    <link rel="apple-touch-icon" href="./images/qbittorrent32.png">
 | 
				
			||||||
</head>
 | 
					</head>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<body>
 | 
					<body>
 | 
				
			||||||
@@ -31,10 +31,10 @@
 | 
				
			|||||||
        <a href="" class="reload">Reload</a>
 | 
					        <a href="" class="reload">Reload</a>
 | 
				
			||||||
        <a class="dismiss">🗙</a>
 | 
					        <a class="dismiss">🗙</a>
 | 
				
			||||||
    </div>
 | 
					    </div>
 | 
				
			||||||
    <script src="_framework/blazor.webassembly.js"></script>
 | 
					    <script src="./_framework/blazor.webassembly.js"></script>
 | 
				
			||||||
    <script src="_content/MudBlazor/MudBlazor.min.js"></script>
 | 
					    <script src="./_content/MudBlazor/MudBlazor.min.js"></script>
 | 
				
			||||||
    <script src="js/piecesbar.js"></script>
 | 
					    <script src="./js/piecesbar.js"></script>
 | 
				
			||||||
    <script src="js/interop.js"></script>
 | 
					    <script src="./js/interop.js"></script>
 | 
				
			||||||
</body>
 | 
					</body>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
</html>
 | 
					</html>
 | 
				
			||||||
@@ -5,4 +5,4 @@
 | 
				
			|||||||
// * @author John Doherty <www.johndoherty.info>
 | 
					// * @author John Doherty <www.johndoherty.info>
 | 
				
			||||||
// * @license MIT
 | 
					// * @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);
 | 
					                writer.WriteNumberValue(0);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else if (value.IsDefaltFolder)
 | 
					            else if (value.IsDefaultFolder)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                writer.WriteNumberValue(1);
 | 
					                writer.WriteNumberValue(1);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,7 @@
 | 
				
			|||||||
<Project Sdk="Microsoft.NET.Sdk">
 | 
					<Project Sdk="Microsoft.NET.Sdk">
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  <PropertyGroup>
 | 
					  <PropertyGroup>
 | 
				
			||||||
    <TargetFramework>net8.0</TargetFramework>
 | 
					    <TargetFramework>net9.0</TargetFramework>
 | 
				
			||||||
    <ImplicitUsings>enable</ImplicitUsings>
 | 
					    <ImplicitUsings>enable</ImplicitUsings>
 | 
				
			||||||
    <Nullable>enable</Nullable>
 | 
					    <Nullable>enable</Nullable>
 | 
				
			||||||
	  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
						  <TreatWarningsAsErrors>true</TreatWarningsAsErrors>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -112,7 +112,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
				
			|||||||
            int maxConnecPerTorrent,
 | 
					            int maxConnecPerTorrent,
 | 
				
			||||||
            int maxInactiveSeedingTime,
 | 
					            int maxInactiveSeedingTime,
 | 
				
			||||||
            bool maxInactiveSeedingTimeEnabled,
 | 
					            bool maxInactiveSeedingTimeEnabled,
 | 
				
			||||||
            int maxRatio,
 | 
					            float maxRatio,
 | 
				
			||||||
            int maxRatioAct,
 | 
					            int maxRatioAct,
 | 
				
			||||||
            bool maxRatioEnabled,
 | 
					            bool maxRatioEnabled,
 | 
				
			||||||
            int maxSeedingTime,
 | 
					            int maxSeedingTime,
 | 
				
			||||||
@@ -745,7 +745,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
				
			|||||||
        public bool MaxInactiveSeedingTimeEnabled { get; }
 | 
					        public bool MaxInactiveSeedingTimeEnabled { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("max_ratio")]
 | 
					        [JsonPropertyName("max_ratio")]
 | 
				
			||||||
        public int MaxRatio { get; }
 | 
					        public float MaxRatio { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("max_ratio_act")]
 | 
					        [JsonPropertyName("max_ratio_act")]
 | 
				
			||||||
        public int MaxRatioAct { get; }
 | 
					        public int MaxRatioAct { get; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        public bool IsWatchedFolder { get; set; }
 | 
					        public bool IsWatchedFolder { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public bool IsDefaltFolder { get; set; }
 | 
					        public bool IsDefaultFolder { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        public string? SavePath { get; set; }
 | 
					        public string? SavePath { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -23,7 +23,7 @@
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    return new SaveLocation
 | 
					                    return new SaveLocation
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        IsDefaltFolder = true
 | 
					                        IsDefaultFolder = true
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -40,7 +40,7 @@
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    return new SaveLocation
 | 
					                    return new SaveLocation
 | 
				
			||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        IsDefaltFolder = true
 | 
					                        IsDefaultFolder = true
 | 
				
			||||||
                    };
 | 
					                    };
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                else
 | 
					                else
 | 
				
			||||||
@@ -61,7 +61,7 @@
 | 
				
			|||||||
            {
 | 
					            {
 | 
				
			||||||
                return 0;
 | 
					                return 0;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
            else if (IsDefaltFolder)
 | 
					            else if (IsDefaultFolder)
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                return 1;
 | 
					                return 1;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
				
			|||||||
            long downloadLimit,
 | 
					            long downloadLimit,
 | 
				
			||||||
            long downloadSpeed,
 | 
					            long downloadSpeed,
 | 
				
			||||||
            long downloadSpeedAverage,
 | 
					            long downloadSpeedAverage,
 | 
				
			||||||
            int estimatedTimeOfArrival,
 | 
					            long estimatedTimeOfArrival,
 | 
				
			||||||
            long lastSeen,
 | 
					            long lastSeen,
 | 
				
			||||||
            int connections,
 | 
					            int connections,
 | 
				
			||||||
            int connectionsLimit,
 | 
					            int connectionsLimit,
 | 
				
			||||||
@@ -104,7 +104,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
				
			|||||||
        public long DownloadSpeedAverage { get; }
 | 
					        public long DownloadSpeedAverage { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("eta")]
 | 
					        [JsonPropertyName("eta")]
 | 
				
			||||||
        public int EstimatedTimeOfArrival { get; }
 | 
					        public long EstimatedTimeOfArrival { get; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("last_seen")]
 | 
					        [JsonPropertyName("last_seen")]
 | 
				
			||||||
        public long LastSeen { get; }
 | 
					        public long LastSeen { get; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -323,7 +323,7 @@ namespace Lantean.QBitTorrentClient.Models
 | 
				
			|||||||
        public bool? MaxInactiveSeedingTimeEnabled { get; set; }
 | 
					        public bool? MaxInactiveSeedingTimeEnabled { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("max_ratio")]
 | 
					        [JsonPropertyName("max_ratio")]
 | 
				
			||||||
        public int? MaxRatio { get; set; }
 | 
					        public float? MaxRatio { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [JsonPropertyName("max_ratio_act")]
 | 
					        [JsonPropertyName("max_ratio_act")]
 | 
				
			||||||
        public int? MaxRatioAct { get; set; }
 | 
					        public int? MaxRatioAct { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										5
									
								
								global.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								global.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,5 @@
 | 
				
			|||||||
 | 
					{
 | 
				
			||||||
 | 
					  "sdk": {
 | 
				
			||||||
 | 
					    "version": "9.0.306"
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										11
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								nuget.config
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
				
			|||||||
 | 
					<configuration>
 | 
				
			||||||
 | 
					  <packageSources>
 | 
				
			||||||
 | 
					    <!-- Define package sources here -->
 | 
				
			||||||
 | 
					  </packageSources>
 | 
				
			||||||
 | 
					  <packageSourceMapping>
 | 
				
			||||||
 | 
					    <!-- Optional source mapping -->
 | 
				
			||||||
 | 
					  </packageSourceMapping>
 | 
				
			||||||
 | 
					  <packageVersionOverride>
 | 
				
			||||||
 | 
					    <package id="FluentAssertions" allowedVersions="[7.0.0,8.0.0)" />
 | 
				
			||||||
 | 
					  </packageVersionOverride>
 | 
				
			||||||
 | 
					</configuration>
 | 
				
			||||||
							
								
								
									
										94
									
								
								readme.md
									
									
									
									
									
								
							
							
						
						
									
										94
									
								
								readme.md
									
									
									
									
									
								
							@@ -1,14 +1,84 @@
 | 
				
			|||||||
# qbt-mud
 | 
					# qbtmud
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## To-Do
 | 
					qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Rename multiple files dialog
 | 
					## Features
 | 
				
			||||||
- ~~RSS feeds and dialogs~~
 | 
					
 | 
				
			||||||
- ~~About~~
 | 
					qbtmud replicates all core features of the qBittorrent WebUI, including:
 | 
				
			||||||
- ~~Context menu for files list/trackers list/peers list~~
 | 
					
 | 
				
			||||||
- ~~Tag management page~~
 | 
					- **Torrent Management** – Add, remove, and control torrents.
 | 
				
			||||||
- ~~Category management page~~
 | 
					- **Tracker Control** – View and manage trackers.
 | 
				
			||||||
- ~~Update all tables to use DynamicTable~~
 | 
					- **Peer Management** – Monitor and manage peers connected to torrents.
 | 
				
			||||||
  - ~~Log~~
 | 
					- **File Prioritization** – Select and prioritize specific files within a torrent.
 | 
				
			||||||
  - ~~Blocks~~
 | 
					- **Speed Limits** – Set global and per-torrent speed limits.
 | 
				
			||||||
  - ~~Search~~
 | 
					- **RSS Integration** – Subscribe to RSS feeds for automated torrent downloads.
 | 
				
			||||||
 | 
					- **Search Functionality** – Integrated torrent search.
 | 
				
			||||||
 | 
					- **Sequential Downloading** – Download files in order for media streaming.
 | 
				
			||||||
 | 
					- **Super Seeding Mode** – Efficiently distribute torrents as an initial seeder.
 | 
				
			||||||
 | 
					- **IP Filtering** – Improve security by filtering specific IP addresses.
 | 
				
			||||||
 | 
					- **IPv6 Support** – Full support for IPv6 networks.
 | 
				
			||||||
 | 
					- **Bandwidth Scheduler** – Schedule bandwidth limits.
 | 
				
			||||||
 | 
					- **WebUI Access** – Remotely manage torrents through the WebUI.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For a detailed explanation of these features, refer to the [qBittorrent Options Guide](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Installation
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To install qbtmud without building from source:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1. Download the Latest Release
 | 
				
			||||||
 | 
					- Go to the [qbtmud Releases](https://github.com/lantean-code/qbtmud/releases) page.
 | 
				
			||||||
 | 
					- Download the latest release archive for your operating system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2. Extract the Archive
 | 
				
			||||||
 | 
					- Extract the contents of the downloaded archive to a directory of your choice.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3. Configure qBittorrent to Use qbtmud
 | 
				
			||||||
 | 
					- Open qBittorrent and navigate to `Tools` > `Options` > `Web UI`.
 | 
				
			||||||
 | 
					- Enable the option **"Use alternative WebUI"**.
 | 
				
			||||||
 | 
					- Set the **"Root Folder"** to the directory where you extracted qbtmud.
 | 
				
			||||||
 | 
					- Click **OK** to save the settings.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 4. Access qbtmud
 | 
				
			||||||
 | 
					- Open your web browser and go to `http://localhost:8080` (or the port configured in qBittorrent).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					For more detailed instructions, refer to the [Alternate WebUI Usage Guide](https://github.com/qbittorrent/qBittorrent/wiki/Alternate-WebUI-usage).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					## Building from Source
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					To build qbtmud from source, you need to have the **.NET 9.0 SDK** installed on your system.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 1. Clone the Repository
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					git clone https://github.com/lantean-code/qbtmud.git
 | 
				
			||||||
 | 
					cd qbtmud
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 2. Restore Dependencies
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					dotnet restore
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 3. Build and Publish the Application
 | 
				
			||||||
 | 
					```sh
 | 
				
			||||||
 | 
					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.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### 5. Run qbtmud
 | 
				
			||||||
 | 
					Navigate to the directory containing the built files and run the application using the appropriate command for your OS.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					By following these steps, you can set up qbtmud to manage your qBittorrent server with an improved web interface, offering better functionality and usability.
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user