mirror of
				https://github.com/lantean-code/qbtmud.git
				synced 2025-10-31 03:53:36 +00:00 
			
		
		
		
	Compare commits
	
		
			45 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 | 
							
								
								
									
										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 | ||||||
|   | |||||||
| @@ -4,24 +4,20 @@ | |||||||
|     <TargetFramework>net9.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="7.1.0" AllowedVersions="[5.0.0,7.*.*)" /> |     <PackageReference Include="AwesomeAssertions" Version="9.2.1" /> | ||||||
|     <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" /> |     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" /> | ||||||
|     <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" /> |  | ||||||
|     <PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" /> |  | ||||||
|     <PackageReference Include="MudBlazor" Version="8.2.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.3" /> |     <PackageReference Include="xunit" Version="2.9.3" /> | ||||||
|     <PackageReference Include="xunit.runner.visualstudio" Version="3.0.1"> |     <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; } | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs | |||||||
|         protected IDialogService DialogService { get; set; } = default!; |         protected IDialogService DialogService { get; set; } = default!; | ||||||
|  |  | ||||||
|         [CascadingParameter] |         [CascadingParameter] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Parameter] |         [Parameter] | ||||||
|         public string? Url { get; set; } |         public string? Url { get; set; } | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| using Lantean.QBitTorrentClient; | using Lantean.QBitTorrentClient; | ||||||
| using Lantean.QBTMud.Models; | using Lantean.QBTMud.Models; | ||||||
| using Microsoft.AspNetCore.Components; | using Microsoft.AspNetCore.Components; | ||||||
| using MudBlazor; |  | ||||||
|  |  | ||||||
| namespace Lantean.QBTMud.Components.Dialogs | namespace Lantean.QBTMud.Components.Dialogs | ||||||
| { | { | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs | |||||||
|     public partial class AddTrackerDialog |     public partial class AddTrackerDialog | ||||||
|     { |     { | ||||||
|         [CascadingParameter] |         [CascadingParameter] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Inject] |         [Inject] | ||||||
|         protected IApiClient ApiClient { get; set; } = default!; |         protected IApiClient ApiClient { get; set; } = default!; | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs | |||||||
|     public partial class ConfirmDialog |     public partial class ConfirmDialog | ||||||
|     { |     { | ||||||
|         [CascadingParameter] |         [CascadingParameter] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Parameter] |         [Parameter] | ||||||
|         public string Content { get; set; } = default!; |         public string Content { get; set; } = default!; | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs | |||||||
|     public partial class DeleteDialog |     public partial class DeleteDialog | ||||||
|     { |     { | ||||||
|         [CascadingParameter] |         [CascadingParameter] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Parameter] |         [Parameter] | ||||||
|         public string? Label { get; set; } |         public string? Label { get; set; } | ||||||
|   | |||||||
| @@ -30,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs | |||||||
|         protected ILocalStorageService LocalStorage { get; set; } = default!; |         protected ILocalStorageService LocalStorage { get; set; } = default!; | ||||||
|  |  | ||||||
|         [CascadingParameter] |         [CascadingParameter] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Parameter] |         [Parameter] | ||||||
|         public string? Hash { get; set; } |         public string? Hash { get; set; } | ||||||
| @@ -426,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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance 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] | ||||||
|         IMudDialogInstance MudDialog { get; set; } = default!; |         private IMudDialogInstance MudDialog { get; set; } = default!; | ||||||
|  |  | ||||||
|         [Parameter] |         [Parameter] | ||||||
|         [EditorRequired] |         [EditorRequired] | ||||||
|   | |||||||
| @@ -1,9 +1,10 @@ | |||||||
| <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 style="overflow-x: auto; white-space: nowrap; width: 100%;"> | <div class="content-panel"> | ||||||
| <MudToolBar Gutters="false" Dense="true"> |     <div class="content-panel__toolbar content-panel__toolbar--scroll"> | ||||||
|  |         <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" /> | ||||||
|             <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" /> | ||||||
| @@ -22,10 +23,10 @@ | |||||||
|             <MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" /> |             <MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" /> | ||||||
|             <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> | ||||||
|  |     <div class="content-panel__body"> | ||||||
| <DynamicTable |         <DynamicTable | ||||||
|             @ref="Table" |             @ref="Table" | ||||||
|             T="ContentItem"  |             T="ContentItem"  | ||||||
|             ColumnDefinitions="Columns"  |             ColumnDefinitions="Columns"  | ||||||
| @@ -38,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,19 +1,23 @@ | |||||||
| <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> | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton> |             <MudIconButton Icon="@Icons.Material.Filled.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> | ||||||
|  |  | ||||||
| <DynamicTable T="Peer" |     <div class="content-panel__body"> | ||||||
|  |         <DynamicTable T="Peer" | ||||||
|                       ColumnDefinitions="Columns" |                       ColumnDefinitions="Columns" | ||||||
|                       Items="Peers" |                       Items="Peers" | ||||||
|                       MultiSelection="false" |                       MultiSelection="false" | ||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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; | ||||||
|  |  | ||||||
| @@ -74,30 +71,7 @@ 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() | ||||||
|         { |         { | ||||||
| @@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components | |||||||
|                     thereAreFirstLastPiecePrio = true; |                     thereAreFirstLastPiecePrio = true; | ||||||
|                 } |                 } | ||||||
|  |  | ||||||
|                 if (torrent.Progress > 0.999999) // 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,18 +6,22 @@ | |||||||
|         <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> | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             <MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton> |             <MudIconButton Icon="@Icons.Material.Filled.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> | ||||||
|             <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton> |             <MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton> | ||||||
|             <MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton> |             <MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</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> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |     <div class="content-panel__body"> | ||||||
|  |         <DynamicTable @ref="Table" | ||||||
|                       T="Lantean.QBitTorrentClient.Models.TorrentTracker" |                       T="Lantean.QBitTorrentClient.Models.TorrentTracker" | ||||||
|                       ColumnDefinitions="Columns" |                       ColumnDefinitions="Columns" | ||||||
|                       Items="Trackers" |                       Items="Trackers" | ||||||
| @@ -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,26 +0,0 @@ | |||||||
| @inherits MudComponentBase |  | ||||||
|  |  | ||||||
| <MudMenu @ref="FakeMenu" Style="display: none" OpenChanged="FakeOpenChanged"></MudMenu> |  | ||||||
|  |  | ||||||
| @* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@ |  | ||||||
| <MudPopover tracker="@Id" |  | ||||||
|             Open="@_open" |  | ||||||
|             Class="unselectable" |  | ||||||
|             MaxHeight="@MaxHeight" |  | ||||||
|             AnchorOrigin="@AnchorOrigin" |  | ||||||
|             TransformOrigin="@TransformOrigin" |  | ||||||
|             RelativeWidth="@RelativeWidth" |  | ||||||
|             OverflowBehavior="OverflowBehavior.FlipAlways" |  | ||||||
|             Style="@_popoverStyle" |  | ||||||
|             @ontouchend:preventDefault> |  | ||||||
|     <CascadingValue Value="@(FakeMenu)"> |  | ||||||
|         @if (_showChildren) |  | ||||||
|         { |  | ||||||
|             <MudList T="object" Class="unselectable"  Dense="@Dense"> |  | ||||||
|                 @ChildContent |  | ||||||
|             </MudList> |  | ||||||
|         } |  | ||||||
|     </CascadingValue> |  | ||||||
| </MudPopover> |  | ||||||
|  |  | ||||||
| <MudOverlay Visible="@(_open)" LockScroll="@LockScroll" AutoClose="true" OnClosed="@CloseMenuAsync" /> |  | ||||||
| @@ -1,290 +0,0 @@ | |||||||
| using Lantean.QBTMud.Interop; |  | ||||||
| using Microsoft.AspNetCore.Components; |  | ||||||
| using Microsoft.AspNetCore.Components.Web; |  | ||||||
| using Microsoft.JSInterop; |  | ||||||
| using MudBlazor; |  | ||||||
| using MudBlazor.Utilities; |  | ||||||
|  |  | ||||||
| namespace Lantean.QBTMud.Components.UI |  | ||||||
| { |  | ||||||
|     public partial class ContextMenu : MudComponentBase |  | ||||||
|     { |  | ||||||
|         private bool _open; |  | ||||||
|         private bool _showChildren; |  | ||||||
|         private string? _popoverStyle; |  | ||||||
|         private string? _id; |  | ||||||
|  |  | ||||||
|         private double _x; |  | ||||||
|         private double _y; |  | ||||||
|         private bool _isResized = false; |  | ||||||
|  |  | ||||||
|         private const double _diff = 64; |  | ||||||
|  |  | ||||||
|         private string Id |  | ||||||
|         { |  | ||||||
|             get |  | ||||||
|             { |  | ||||||
|                 _id ??= Guid.NewGuid().ToString(); |  | ||||||
|  |  | ||||||
|                 return _id; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         [Inject] |  | ||||||
|         public IJSRuntime JSRuntime { get; set; } = default!; |  | ||||||
|  |  | ||||||
|         [Inject] |  | ||||||
|         public IPopoverService PopoverService { get; set; } = default!; |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// If true, compact vertical padding will be applied to all menu items. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public bool Dense { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Set to true if you want to prevent page from scrolling when the menu is open |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public bool LockScroll { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// If true, the list menu will be same width as the parent. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public DropdownWidth RelativeWidth { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Sets the max height the menu can have when open. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public int? MaxHeight { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Set the anchor origin point to determine where the popover will open from. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public Origin AnchorOrigin { get; set; } = Origin.TopLeft; |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Sets the transform origin point for the popover. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupAppearance)] |  | ||||||
|         public Origin TransformOrigin { get; set; } = Origin.TopLeft; |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// If true, menu will be disabled. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.Behavior)] |  | ||||||
|         public bool Disabled { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.Appearance)] |  | ||||||
|         public bool Ripple { get; set; } = true; |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Determines whether the component has a drop-shadow. Default is true |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.Appearance)] |  | ||||||
|         public bool DropShadow { get; set; } = true; |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Add menu items here |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupBehavior)] |  | ||||||
|         public RenderFragment? ChildContent { get; set; } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Fired when the menu <see cref="Open"/> property changes. |  | ||||||
|         /// </summary> |  | ||||||
|         [Parameter] |  | ||||||
|         [Category(CategoryTypes.Menu.PopupBehavior)] |  | ||||||
|         public EventCallback<bool> OpenChanged { get; set; } |  | ||||||
|  |  | ||||||
|         [Parameter] |  | ||||||
|         public int AdjustmentX { get; set; } |  | ||||||
|  |  | ||||||
|         [Parameter] |  | ||||||
|         public int AdjustmentY { get; set; } |  | ||||||
|  |  | ||||||
|         protected MudMenu? FakeMenu { get; set; } |  | ||||||
|  |  | ||||||
|         protected void FakeOpenChanged(bool value) |  | ||||||
|         { |  | ||||||
|             if (!value) |  | ||||||
|             { |  | ||||||
|                 _open = false; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             StateHasChanged(); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Opens the menu. |  | ||||||
|         /// </summary> |  | ||||||
|         /// <param name="args"> |  | ||||||
|         /// The arguments of the calling mouse/pointer event. |  | ||||||
|         /// </param> |  | ||||||
|         public async Task OpenMenuAsync(EventArgs args) |  | ||||||
|         { |  | ||||||
|             if (Disabled) |  | ||||||
|             { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             // long press on iOS triggers selection, so clear it |  | ||||||
|             await JSRuntime.ClearSelection(); |  | ||||||
|  |  | ||||||
|             if (args is not LongPressEventArgs) |  | ||||||
|             { |  | ||||||
|                 _showChildren = true; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             _open = true; |  | ||||||
|             _isResized = false; |  | ||||||
|             StateHasChanged(); |  | ||||||
|  |  | ||||||
|             var (x, y) = GetPositionFromArgs(args); |  | ||||||
|             _x = x; |  | ||||||
|             _y = y; |  | ||||||
|  |  | ||||||
|             SetPopoverStyle(x, y); |  | ||||||
|  |  | ||||||
|             StateHasChanged(); |  | ||||||
|  |  | ||||||
|             await OpenChanged.InvokeAsync(_open); |  | ||||||
|  |  | ||||||
|             // long press on iOS triggers selection, so clear it |  | ||||||
|             await JSRuntime.ClearSelection(); |  | ||||||
|  |  | ||||||
|             if (args is LongPressEventArgs) |  | ||||||
|             { |  | ||||||
|                 await Task.Delay(1000); |  | ||||||
|                 _showChildren = true; |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Closes the menu. |  | ||||||
|         /// </summary> |  | ||||||
|         public Task CloseMenuAsync() |  | ||||||
|         { |  | ||||||
|             _open = false; |  | ||||||
|             _popoverStyle = null; |  | ||||||
|             StateHasChanged(); |  | ||||||
|  |  | ||||||
|             return OpenChanged.InvokeAsync(_open); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         private void SetPopoverStyle(double x, double y) |  | ||||||
|         { |  | ||||||
|             _popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};"; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         /// <summary> |  | ||||||
|         /// Toggle the visibility of the menu. |  | ||||||
|         /// </summary> |  | ||||||
|         public async Task ToggleMenuAsync(EventArgs args) |  | ||||||
|         { |  | ||||||
|             if (Disabled) |  | ||||||
|             { |  | ||||||
|                 return; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             if (_open) |  | ||||||
|             { |  | ||||||
|                 await CloseMenuAsync(); |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 await OpenMenuAsync(args); |  | ||||||
|             } |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         protected override Task OnAfterRenderAsync(bool firstRender) |  | ||||||
|         { |  | ||||||
|             if (!_isResized) |  | ||||||
|             { |  | ||||||
|                 //await DeterminePosition(); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return Task.CompletedTask; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         //private async Task DeterminePosition() |  | ||||||
|         //{ |  | ||||||
|         //    var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content"); |  | ||||||
|         //    double? contextMenuHeight = null; |  | ||||||
|         //    double? contextMenuWidth = null; |  | ||||||
|  |  | ||||||
|         //    var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id); |  | ||||||
|  |  | ||||||
|         //    var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}"); |  | ||||||
|         //    if (popoverSize.Height > 0) |  | ||||||
|         //    { |  | ||||||
|         //        contextMenuHeight = popoverSize.Height; |  | ||||||
|         //        contextMenuWidth = popoverSize.Width; |  | ||||||
|         //    } |  | ||||||
|         //    else |  | ||||||
|         //    { |  | ||||||
|         //        return; |  | ||||||
|         //    } |  | ||||||
|  |  | ||||||
|         //    // the bottom position of the popover will be rendered off screen |  | ||||||
|         //    if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height) |  | ||||||
|         //    { |  | ||||||
|         //        // adjust the top of the context menu |  | ||||||
|         //        var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value)); |  | ||||||
|         //        _y -= overshoot; |  | ||||||
|  |  | ||||||
|         //        if (_y - _diff + contextMenuHeight >= mainContentSize.Height) |  | ||||||
|         //        { |  | ||||||
|         //            MaxHeight = (int)(mainContentSize.Height - _y + _diff); |  | ||||||
|         //        } |  | ||||||
|         //    } |  | ||||||
|  |  | ||||||
|         //    if (_x + contextMenuWidth.Value > mainContentSize.Width) |  | ||||||
|         //    { |  | ||||||
|         //        var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value)); |  | ||||||
|         //        _x -= overshoot; |  | ||||||
|         //    } |  | ||||||
|  |  | ||||||
|         //    SetPopoverStyle(_x, _y); |  | ||||||
|         //    _isResized = true; |  | ||||||
|         //    await InvokeAsync(StateHasChanged); |  | ||||||
|         //} |  | ||||||
|  |  | ||||||
|         private (double x, double y) GetPositionFromArgs(EventArgs eventArgs) |  | ||||||
|         { |  | ||||||
|             double x, y; |  | ||||||
|             if (eventArgs is MouseEventArgs mouseEventArgs) |  | ||||||
|             { |  | ||||||
|                 x = mouseEventArgs.ClientX; |  | ||||||
|                 y = mouseEventArgs.ClientY; |  | ||||||
|             } |  | ||||||
|             else if (eventArgs is LongPressEventArgs longPressEventArgs) |  | ||||||
|             { |  | ||||||
|                 x = longPressEventArgs.ClientX; |  | ||||||
|                 y = longPressEventArgs.ClientY; |  | ||||||
|             } |  | ||||||
|             else |  | ||||||
|             { |  | ||||||
|                 throw new NotSupportedException("Invalid eventArgs type."); |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|             return (x + AdjustmentX, y + AdjustmentY); |  | ||||||
|         } |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| @@ -1,5 +1,5 @@ | |||||||
| <div class="@Classname"> | <div class="@Classname"> | ||||||
|     <div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault> |     <div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault> | ||||||
|         @if (!string.IsNullOrEmpty(Icon)) |         @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 => | ||||||
|   | |||||||
| @@ -81,6 +81,8 @@ 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 Dictionary<string, int> _columnOrder = []; | ||||||
| @@ -89,8 +91,16 @@ namespace Lantean.QBTMud.Components.UI | |||||||
|  |  | ||||||
|         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; | ||||||
| @@ -109,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; | ||||||
| @@ -137,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() | ||||||
| @@ -165,39 +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) |             if (_columnOrder.Count == 0) | ||||||
|             { |             { | ||||||
|                 foreach (var column in filteredColumns) |                 orderedColumns = filteredColumns; | ||||||
|  |             } | ||||||
|  |             else | ||||||
|             { |             { | ||||||
|                     if (_columnWidths.TryGetValue(column.Id, out var value)) |                 var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList(); | ||||||
|                     { |  | ||||||
|                         column.Width = value; |  | ||||||
|                     } |  | ||||||
|  |  | ||||||
|                     yield return column; |  | ||||||
|                 } |  | ||||||
|  |  | ||||||
|                 yield break; |  | ||||||
|             } |  | ||||||
|  |  | ||||||
|                 var columnDictionary = filteredColumns.ToDictionary(c => c.Id); |                 var columnDictionary = filteredColumns.ToDictionary(c => c.Id); | ||||||
|             foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key)) |                 orderedColumns = new List<ColumnDefinition<T>>(filteredColumns.Count); | ||||||
|  |  | ||||||
|  |                 foreach (var (columnId, _) in orderLookup) | ||||||
|                 { |                 { | ||||||
|                     if (!columnDictionary.TryGetValue(columnId, out var column)) |                     if (!columnDictionary.TryGetValue(columnId, out var column)) | ||||||
|                     { |                     { | ||||||
|                         continue; |                         continue; | ||||||
|                     } |                     } | ||||||
|  |  | ||||||
|  |                     orderedColumns.Add(column); | ||||||
|  |                 } | ||||||
|  |  | ||||||
|  |                 if (orderedColumns.Count != filteredColumns.Count) | ||||||
|  |                 { | ||||||
|  |                     var existingIds = new HashSet<string>(orderedColumns.Select(c => c.Id)); | ||||||
|  |                     foreach (var column in filteredColumns) | ||||||
|  |                     { | ||||||
|  |                         if (existingIds.Add(column.Id)) | ||||||
|  |                         { | ||||||
|  |                             orderedColumns.Add(column); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             foreach (var column in orderedColumns) | ||||||
|  |             { | ||||||
|                 if (_columnWidths.TryGetValue(column.Id, out var value)) |                 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) | ||||||
| @@ -223,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; | ||||||
| @@ -298,6 +374,7 @@ 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)); | ||||||
|         } |         } | ||||||
| @@ -316,18 +393,21 @@ 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)) |             if (!DictionaryEqual(_columnOrder, result.ColumnOrder)) | ||||||
|             { |             { | ||||||
|                 _columnOrder = result.ColumnOrder; |                 _columnOrder = result.ColumnOrder; | ||||||
|                 await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder); |                 await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder); | ||||||
|  |                 MarkColumnsDirty(); | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |  | ||||||
| @@ -368,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> | ||||||
| @@ -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; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -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="9.0.1" /> | 	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" /> | ||||||
| 	<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" /> | 	  <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" /> | ||||||
| 	<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" /> | 	  <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" /> | ||||||
| 	<PackageReference Include="MudBlazor" Version="8.2.0" /> | 	  <PackageReference Include="MudBlazor" Version="8.13.0" /> | ||||||
| 	  <PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" /> | 	  <PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" /> | ||||||
|     <!-- added to fix vuln in dependency --> |  | ||||||
| 	<PackageReference Include="System.Text.Json" Version="9.0.1" /> |  | ||||||
|   </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,20 +24,9 @@ | |||||||
|                                                 <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); z-index: 900"> |  | ||||||
|                                                                     @if (MainData?.LostConnection == true) |                                                                     @if (MainData?.LostConnection == true) | ||||||
|                                                                     { |                                                                     { | ||||||
|                                                                         <MudText Class="mx-2 mb-1 d-none d-sm-flex" 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> | ||||||
| @@ -49,7 +39,7 @@ | |||||||
|                                                                     @{ |                                                                     @{ | ||||||
|                                                                         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" Class="" /> |                                                                     <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" Class="" /> |                                                                     <MudDivider Vertical="true" Class="" /> | ||||||
| @@ -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) | ||||||
|   | |||||||
| @@ -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,13 +75,13 @@ 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; | ||||||
|   | |||||||
| @@ -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,18 +1,22 @@ | |||||||
| @page "/about" | @page "/about" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
|                 <MudDivider Vertical="true" /> |                 <MudDivider Vertical="true" /> | ||||||
|             } |             } | ||||||
|             <MudText Class="px-5 no-wrap">About</MudText> |             <MudText Class="px-5 no-wrap">About</MudText> | ||||||
| </MudToolBar> |         </MudToolBar> | ||||||
|  |     </div> | ||||||
|  |  | ||||||
| <MudTabs Elevation="2" ApplyEffectsToContainer="true"> |     <div class="content-panel__body"> | ||||||
|  |         <MudTabs Elevation="2" ApplyEffectsToContainer="true"> | ||||||
|             <MudTabPanel Text="About"> |             <MudTabPanel Text="About"> | ||||||
|         <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3"> |                 <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 content-panel__container options-tab-contents"> | ||||||
|             <MudGrid Class="mt-0 mb-4"> |             <MudGrid Class="mt-0 mb-4"> | ||||||
|                 <MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center"> |                 <MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center"> | ||||||
|                     <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" |                     <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" | ||||||
| @@ -60,7 +64,7 @@ | |||||||
|         </MudContainer> |         </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.h5" 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"> | ||||||
| @@ -108,7 +112,7 @@ | |||||||
|         </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> | ||||||
| @@ -118,7 +122,7 @@ | |||||||
|         </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> | ||||||
| @@ -168,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+), | ||||||
| @@ -1061,7 +1065,7 @@ | |||||||
|         </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"> | ||||||
| @@ -1105,3 +1109,5 @@ | |||||||
|         </MudContainer> |         </MudContainer> | ||||||
|     </MudTabPanel> |     </MudTabPanel> | ||||||
| </MudTabs> | </MudTabs> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/blocks" | @page "/blocks" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -9,9 +11,10 @@ | |||||||
|             } |             } | ||||||
|             <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> | ||||||
| <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> |     <div class="content-panel__body"> | ||||||
|  |         <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> | ||||||
|             <MudCardContent> |             <MudCardContent> | ||||||
|                 <EditForm Model="Model" OnSubmit="Submit"> |                 <EditForm Model="Model" OnSubmit="Submit"> | ||||||
|                     <MudGrid> |                     <MudGrid> | ||||||
| @@ -24,13 +27,15 @@ | |||||||
|                     </MudGrid> |                     </MudGrid> | ||||||
|                 </EditForm> |                 </EditForm> | ||||||
|             </MudCardContent> |             </MudCardContent> | ||||||
| </MudCard> |         </MudCard> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |         <DynamicTable @ref="Table" | ||||||
|                       T="Lantean.QBitTorrentClient.Models.PeerLog" |                       T="Lantean.QBitTorrentClient.Models.PeerLog" | ||||||
|                       ColumnDefinitions="Columns" |                       ColumnDefinitions="Columns" | ||||||
|                       Items="Results" |                       Items="Results" | ||||||
|                       MultiSelection="false" |                       MultiSelection="false" | ||||||
|                       SelectOnRowClick="false" |                       SelectOnRowClick="false" | ||||||
|                       RowClassFunc="RowClass" |                       RowClassFunc="RowClass" | ||||||
|               Class="search-list" /> |                       Class="search-list content-panel__table" /> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/categories" | @page "/categories" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -10,15 +12,19 @@ | |||||||
|             <MudText Class="px-5 no-wrap">Categories</MudText> |             <MudText Class="px-5 no-wrap">Categories</MudText> | ||||||
|             <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> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |     <div class="content-panel__body"> | ||||||
|  |         <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,8 +1,9 @@ | |||||||
| @page "/details/{hash}" | @page "/details/{hash}" | ||||||
| @layout DetailsLayout | @layout DetailsLayout | ||||||
|  |  | ||||||
| <div style="overflow-x: auto; white-space: nowrap; width: 100%;"> | <div class="content-panel"> | ||||||
| <MudToolBar Gutters="false" Dense="true"> |     <div class="content-panel__toolbar content-panel__toolbar--scroll"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -14,11 +15,12 @@ | |||||||
|             } |             } | ||||||
|             <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> | ||||||
|  |  | ||||||
| @if (ShowTabs) |     <div class="content-panel__body"> | ||||||
| { |         @if (ShowTabs) | ||||||
|  |         { | ||||||
|             <CascadingValue Value="RefreshInterval" Name="RefreshInterval"> |             <CascadingValue Value="RefreshInterval" Name="RefreshInterval"> | ||||||
|                 <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true"> |                 <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true"> | ||||||
|                     <MudTabPanel Text="General"> |                     <MudTabPanel Text="General"> | ||||||
| @@ -38,4 +40,6 @@ | |||||||
|                     </MudTabPanel> |                     </MudTabPanel> | ||||||
|                 </MudTabs> |                 </MudTabs> | ||||||
|             </CascadingValue> |             </CascadingValue> | ||||||
| } |         } | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/log" | @page "/log" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -9,9 +11,10 @@ | |||||||
|             } |             } | ||||||
|             <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> | ||||||
| <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> |     <div class="content-panel__body"> | ||||||
|  |         <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> | ||||||
|             <MudCardContent> |             <MudCardContent> | ||||||
|                 <EditForm Model="Model" OnSubmit="Submit"> |                 <EditForm Model="Model" OnSubmit="Submit"> | ||||||
|                     <MudGrid> |                     <MudGrid> | ||||||
| @@ -32,13 +35,15 @@ | |||||||
|                     </MudGrid> |                     </MudGrid> | ||||||
|                 </EditForm> |                 </EditForm> | ||||||
|             </MudCardContent> |             </MudCardContent> | ||||||
| </MudCard> |         </MudCard> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |         <DynamicTable @ref="Table" | ||||||
|                       T="Lantean.QBitTorrentClient.Models.Log" |                       T="Lantean.QBitTorrentClient.Models.Log" | ||||||
|                       ColumnDefinitions="Columns" |                       ColumnDefinitions="Columns" | ||||||
|                       Items="Results" |                       Items="Results" | ||||||
|                       MultiSelection="false" |                       MultiSelection="false" | ||||||
|                       SelectOnRowClick="false" |                       SelectOnRowClick="false" | ||||||
|                       RowClassFunc="RowClass" |                       RowClassFunc="RowClass" | ||||||
|               Class="search-list" /> |                       Class="search-list content-panel__table" /> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -3,7 +3,9 @@ | |||||||
|  |  | ||||||
| <NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" /> | <NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" /> | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" /> | ||||||
| @@ -13,31 +15,51 @@ | |||||||
|             <MudDivider Vertical="true" /> |             <MudDivider Vertical="true" /> | ||||||
|             <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> | ||||||
|  |  | ||||||
| <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true"> |     <div class="content-panel__body"> | ||||||
|  |         <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,7 +1,9 @@ | |||||||
| @page "/rss" | @page "/rss" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -14,9 +16,11 @@ | |||||||
|             <MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" /> |             <MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" /> | ||||||
|             <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> | ||||||
| @@ -70,4 +74,6 @@ | |||||||
|                     } |                     } | ||||||
|                 </MudItem> |                 </MudItem> | ||||||
|             </MudGrid> |             </MudGrid> | ||||||
| </MudContainer> |         </MudContainer> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/search" | @page "/search" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -9,9 +11,10 @@ | |||||||
|             } |             } | ||||||
|             <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> | ||||||
| <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> |     <div class="content-panel__body"> | ||||||
|  |         <MudCard Elevation="1" Class="ml-4 mr-4 mb-4"> | ||||||
|             <MudCardContent> |             <MudCardContent> | ||||||
|                 <EditForm Model="Model" OnValidSubmit="DoSearch"> |                 <EditForm Model="Model" OnValidSubmit="DoSearch"> | ||||||
|                     <MudGrid> |                     <MudGrid> | ||||||
| @@ -51,12 +54,14 @@ | |||||||
|                     </MudGrid> |                     </MudGrid> | ||||||
|                 </EditForm> |                 </EditForm> | ||||||
|             </MudCardContent> |             </MudCardContent> | ||||||
| </MudCard> |         </MudCard> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |         <DynamicTable @ref="Table" | ||||||
|                       T="Lantean.QBitTorrentClient.Models.SearchResult" |                       T="Lantean.QBitTorrentClient.Models.SearchResult" | ||||||
|                       ColumnDefinitions="Columns" |                       ColumnDefinitions="Columns" | ||||||
|                       Items="Results" |                       Items="Results" | ||||||
|                       MultiSelection="false" |                       MultiSelection="false" | ||||||
|                       SelectOnRowClick="false" |                       SelectOnRowClick="false" | ||||||
|               Class="search-list" /> |                       Class="search-list content-panel__table" /> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/statistics" | @page "/statistics" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -9,9 +11,11 @@ | |||||||
|             } |             } | ||||||
|             <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"> | ||||||
| @@ -59,4 +63,6 @@ | |||||||
|                     <MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField> |                     <MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField> | ||||||
|                 </MudItem> |                 </MudItem> | ||||||
|             </MudGrid> |             </MudGrid> | ||||||
| </MudContainer> |         </MudContainer> | ||||||
|  |     </div> | ||||||
|  | </div> | ||||||
| @@ -1,7 +1,9 @@ | |||||||
| @page "/tags" | @page "/tags" | ||||||
| @layout OtherLayout | @layout OtherLayout | ||||||
|  |  | ||||||
| <MudToolBar Gutters="false" Dense="true"> | <div class="content-panel"> | ||||||
|  |     <div class="content-panel__toolbar"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             @if (!DrawerOpen) |             @if (!DrawerOpen) | ||||||
|             { |             { | ||||||
|                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> |                 <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" /> | ||||||
| @@ -10,15 +12,19 @@ | |||||||
|             <MudText Class="px-5 no-wrap">Tags</MudText> |             <MudText Class="px-5 no-wrap">Tags</MudText> | ||||||
|             <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> | ||||||
|  |  | ||||||
| <DynamicTable @ref="Table" |     <div class="content-panel__body"> | ||||||
|  |         <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,14 +1,15 @@ | |||||||
| @page "/" | @page "/" | ||||||
| @layout ListLayout | @layout ListLayout | ||||||
|  |  | ||||||
| <ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0"> | <MudMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable"> | ||||||
|     <MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem> |     <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 style="overflow-x: auto; white-space: nowrap; width: 100%;"> | <div class="content-panel"> | ||||||
| <MudToolBar Gutters="false" Dense="true"> |     <div class="content-panel__toolbar content-panel__toolbar--scroll"> | ||||||
|  |         <MudToolBar Gutters="false" Dense="true"> | ||||||
|             <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" /> |             <MudIconButton Icon="@Icons.Material.Outlined.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" /> | ||||||
|             <MudDivider Vertical="true" /> |             <MudDivider Vertical="true" /> | ||||||
| @@ -18,14 +19,14 @@ | |||||||
|             <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" /> | ||||||
|             <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> |     </div> | ||||||
|  |     <div class="content-panel__body"> | ||||||
| <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0"> |         <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"  | ||||||
| @@ -37,7 +38,9 @@ | |||||||
|                 OnTableDataContextMenu="TableDataContextMenu" |                 OnTableDataContextMenu="TableDataContextMenu" | ||||||
|                 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); | ||||||
|   | |||||||
| @@ -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 - 160px); | .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); | ||||||
| @@ -256,3 +314,116 @@ td .folder-button { | |||||||
| .mud-popover .mud-divider:last-child { | .mud-popover .mud-divider:last-child { | ||||||
|     display: none; |     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); | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -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" | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										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