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