33 Commits

Author SHA1 Message Date
ahjephson
0db0ad4374 Remove other v4 logic 2025-10-21 14:20:25 +01:00
ahjephson
c390d83e4d Update to use v5 api only. 2025-10-21 13:38:50 +01:00
ahjephson
8dd29c238d Update client to use net v5 apis 2025-10-21 13:12:38 +01:00
ahjephson
fca17edfd1 Merge tag '1.2.0' into develop
1.2.0
2025-10-20 20:56:10 +01:00
ahjephson
d8535fa262 Merge branch 'release/1.2.0' 2025-10-20 20:55:26 +01:00
ahjephson
1c6bfed6ee Merge pull request #11 from lantean-code/feature/performance-enhancements
Feature/performance enhancements
2025-10-20 20:53:42 +01:00
ahjephson
281caf8026 Format fix correctly 2025-10-20 20:43:47 +01:00
ahjephson
ff905e7cac Fix tab indicator 2025-10-20 20:42:52 +01:00
ahjephson
cb80dd0d6b Fix tabs issue 2025-10-20 20:01:11 +01:00
ahjephson
9113fb90ee Fix statusbar on ios 2025-10-20 18:44:18 +01:00
ahjephson
d8b4e932d1 Fix tabs in About 2025-10-20 17:40:40 +01:00
ahjephson
3d0d211d10 Update layout to remove hacks and rely only on flexbox 2025-10-20 16:39:20 +01:00
ahjephson
7db4f2f78d Fix final issues with longpress. Update all files to use correct encoding and ran through CodeMaid. 2025-10-20 14:54:31 +01:00
ahjephson
1f606b4449 Try fix issue with text selection on longpress 2025-10-20 13:48:05 +01:00
ahjephson
88d66b4887 Fix longpress issue 2025-10-20 13:44:57 +01:00
ahjephson
2ad7be1073 Remove custom ContextMenu and replace with MudMenu 2025-10-20 13:30:40 +01:00
ahjephson
300e81345c Fix status update performance 2025-10-20 11:03:43 +01:00
ahjephson
9d8d84168e Merge pull request #7 from lantean-code/codex/find-and-fix-a-bug
Fix typo in SaveLocation property
2025-10-20 10:15:48 +01:00
ahjephson
bb66b97f45 Merge branch 'develop' into codex/find-and-fix-a-bug 2025-10-20 10:14:47 +01:00
ahjephson
4824037ba7 Fix connection icon 2025-10-20 10:04:50 +01:00
ahjephson
1f9b631a36 Merge bugfixes in 2025-10-20 09:52:55 +01:00
ahjephson
2c744cd972 Fix issue wtih toolbar 2025-10-19 19:13:09 +01:00
ahjephson
b02bb7cfae Fix issues with toolbar not updating 2025-10-19 19:12:10 +01:00
ahjephson
e4dac8556e Improve torrent list performance 2025-10-19 15:21:22 +01:00
ahjephson
a9a8a4eba8 Improve file list performance. 2025-10-19 14:19:21 +01:00
ahjephson
bb524450f0 Fix slowness issues with FilesTab when torrents with large file lists are being rendered. 2025-10-19 11:06:45 +01:00
ahjephson
d4ac79af00 Merge pull request #10 from lantean-code/feature/bugfixes
- Fixed an issue where the tag wasn't being correctly applied to the filter in qBittorrent 5.1+ (#9)
- Fixed an issue where the category wasn't being applied to the filter correctly (#9)
- Fixed invalid ValueChanged for "Default Torrent Management Mode"
- Fixed a crash where TimeSpan.FromSeconds was crashing
- Fixed an invalid icon to appear when Paused/Stopped
2025-10-18 16:18:10 +01:00
ahjephson
7370d73c59 Fix minor display issues 2025-10-18 16:01:53 +01:00
ahjephson
8796cc0f24 Fix #9 and bug related to invalid TimeSpan in duration 2025-10-18 15:37:04 +01:00
ahjephson
b24ae440d4 Merge pull request #8 from ehaughee/develop
Fix MaxRatio to allow float values
2025-10-02 15:01:10 +01:00
Eric Haughee
bb90ce5216 Fix MaxRatio to allow float values 2025-09-20 18:29:18 -07:00
ahjephson
4eaa46b2b3 Fix property name in SaveLocation 2025-06-03 09:02:47 +01:00
ahjephson
1cf9f97187 Merge tag '1.1.0' into develop
1.1.0
2025-05-30 15:46:03 +01:00
103 changed files with 3905 additions and 1874 deletions

3
.gitignore vendored
View File

@@ -360,4 +360,5 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
/output

View File

@@ -10,11 +10,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.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.1.0">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

@@ -1,4 +1,4 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using System.Linq.Expressions;
using System.Text.Json;

View File

@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
readme.md = readme.md
EndProjectSection
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -33,6 +35,10 @@ Global
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.Build.0 = Debug|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.ActiveCfg = Release|Any CPU
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE

View File

@@ -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; } = [];

View File

@@ -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; } = [];

View File

@@ -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; }

View File

@@ -65,4 +65,4 @@
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
</MudGrid>
</MudCollapse>
</MudCollapse>

View File

@@ -1,7 +1,6 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMud.Components.Dialogs
{
@@ -54,7 +53,7 @@ namespace Lantean.QBTMud.Components.Dialogs
TorrentManagementMode = preferences.AutoTmmEnabled;
SavePath = preferences.SavePath;
StartTorrent = !preferences.StartPausedEnabled;
StartTorrent = !preferences.AddStoppedEnabled;
AddToTopOfQueue = preferences.AddToTopOfQueue;
StopCondition = preferences.TorrentStopCondition;
ContentLayout = preferences.TorrentContentLayout;
@@ -79,4 +78,4 @@ namespace Lantean.QBTMud.Components.Dialogs
UploadLimit);
}
}
}
}

View File

@@ -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; } = [];

View File

@@ -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!;

View File

@@ -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!;

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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;

View File

@@ -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; } = [];

View File

@@ -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; } = [];

View File

@@ -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!;

View File

@@ -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; }

View File

@@ -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()
@@ -495,7 +494,7 @@ namespace Lantean.QBTMud.Components.Dialogs
{
var oldPath = renamedFile.Path + renamedFile.OriginalName;
var newPath = renamedFile.Path + renamedFile.NewName;
await ApiClient.RenameFolder(Hash, oldPath, newPath);
}

View File

@@ -53,7 +53,7 @@
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Add paused" Value="AddPaused" ValueChanged="AddPausedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
<MudSelect T="string" Label="Add stopped" Value="AddStopped" ValueChanged="AddStoppedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
<MudSelectItem Value="@("always")">Always</MudSelectItem>
<MudSelectItem Value="@("never")">Never</MudSelectItem>
@@ -103,4 +103,4 @@
<MudButton OnClick="Cancel">Close</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@@ -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!;
@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
SelectedRule.IgnoreDays = value;
}
protected string? AddPaused { get; set; }
protected string? AddStopped { get; set; }
protected void AddPausedChanged(string value)
protected void AddStoppedChanged(string value)
{
AddPaused = value;
AddStopped = value;
switch (value)
{
case "default":
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
switch (SelectedRule.TorrentParams.Stopped)
{
case null:
AddPaused = "default";
AddStopped = "default";
break;
case true:
AddPaused = "always";
AddStopped = "always";
break;
case false:
AddPaused = "never";
AddStopped = "never";
break;
}

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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; }

View File

@@ -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]

View File

@@ -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
{

View File

@@ -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()

View File

@@ -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">
@@ -65,9 +65,9 @@
{
return __builder =>
{
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Stop" IconColor="Color.Warning" OnClick="@(e => StopTorrents(type))">Stop torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
};
}
}
}

View File

@@ -1,11 +1,11 @@
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMud.Components.UI;
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Linq;
namespace Lantean.QBTMud.Components
{
@@ -69,13 +69,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 +154,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 +194,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 +234,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 +273,9 @@ namespace Lantean.QBTMud.Components
ContextMenuTracker = value;
return TrackerContextMenu.OpenMenuAsync(args);
var normalizedArgs = args.NormalizeForContextMenu();
return TrackerContextMenu.OpenMenuAsync(normalizedArgs);
}
protected async Task AddCategory()
@@ -345,18 +353,18 @@ namespace Lantean.QBTMud.Components
}
}
protected async Task ResumeTorrents(string type)
protected async Task StartTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.ResumeTorrents(torrents);
await ApiClient.StartTorrents(hashes: torrents.ToArray());
}
protected async Task PauseTorrents(string type)
protected async Task StopTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.PauseTorrents(torrents);
await ApiClient.StopTorrents(hashes: torrents.ToArray());
}
protected async Task RemoveTorrents(string type)
@@ -470,4 +478,4 @@ namespace Lantean.QBTMud.Components
}
}
}
}
}

View File

@@ -98,4 +98,4 @@
<MudField Label="Comment">@Properties?.Comment</MudField>
</MudItem>
</MudGrid>
</MudContainer>
</MudContainer>

View File

@@ -240,4 +240,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -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" />

View File

@@ -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;

View File

@@ -19,7 +19,7 @@
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Do not start the download automatically" Value="StartPausedEnabled" ValueChanged="StartPausedEnabledChanged" />
<FieldSwitch Label="Do not start the download automatically" Value="AddStoppedEnabled" ValueChanged="AddStoppedEnabledChanged" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
@@ -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>
@@ -306,4 +306,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Options
{
protected string? TorrentContentLayout { get; set; }
protected bool AddToTopOfQueue { get; set; }
protected bool StartPausedEnabled { get; set; }
protected bool AddStoppedEnabled { get; set; }
protected string? TorrentStopCondition { get; set; }
protected bool AutoDeleteMode { get; set; }
protected bool PreallocateAll { get; set; }
@@ -51,7 +51,7 @@ namespace Lantean.QBTMud.Components.Options
// when adding a torrent
TorrentContentLayout = Preferences.TorrentContentLayout;
AddToTopOfQueue = Preferences.AddToTopOfQueue;
StartPausedEnabled = Preferences.StartPausedEnabled;
AddStoppedEnabled = Preferences.AddStoppedEnabled;
TorrentStopCondition = Preferences.TorrentStopCondition;
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
PreallocateAll = Preferences.PreallocateAll;
@@ -116,10 +116,10 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task StartPausedEnabledChanged(bool value)
protected async Task AddStoppedEnabledChanged(bool value)
{
StartPausedEnabled = value;
UpdatePreferences.StartPausedEnabled = value;
AddStoppedEnabled = value;
UpdatePreferences.AddStoppedEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
@@ -410,4 +410,4 @@ namespace Lantean.QBTMud.Components.Options
return null;
}
}
}
}

View File

@@ -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>

View File

@@ -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)

View File

@@ -7,6 +7,7 @@ using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using MudBlazor;
using System.Linq;
namespace Lantean.QBTMud.Components
{
@@ -37,9 +38,6 @@ namespace Lantean.QBTMud.Components
[Inject]
protected IKeyboardService KeyboardService { get; set; } = default!;
[CascadingParameter(Name = "Version")]
public string? Version { get; set; }
[Parameter]
[EditorRequired]
public IEnumerable<string> Hashes { get; set; } = default!;
@@ -71,14 +69,12 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; }
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
protected override void OnInitialized()
{
_actions =
[
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
@@ -146,32 +142,16 @@ namespace Lantean.QBTMud.Components
OverlayVisible = value;
}
protected async Task Pause()
protected async Task Stop()
{
if (MajorVersion < 5)
{
await ApiClient.PauseTorrents(Hashes);
Snackbar.Add("Torrent paused.");
}
else
{
await ApiClient.StopTorrents(Hashes);
Snackbar.Add("Torrent stopped.");
}
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent stopped.");
}
protected async Task Resume()
protected async Task Start()
{
if (MajorVersion < 5)
{
await ApiClient.ResumeTorrents(Hashes);
Snackbar.Add("Torrent resumed.");
}
else
{
await ApiClient.StartTorrents(Hashes);
Snackbar.Add("Torrent started.");
}
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent started.");
}
protected async Task ForceStart()
@@ -385,8 +365,8 @@ namespace Lantean.QBTMud.Components
var allAreFirstLastPiecePrio = true;
var thereAreFirstLastPiecePrio = false;
var allAreDownloaded = true;
var allArePaused = true;
var thereArePaused = false;
var allAreStopped = true;
var thereAreStopped = false;
var allAreForceStart = true;
var thereAreForceStart = false;
var allAreSuperSeeding = true;
@@ -424,27 +404,13 @@ namespace Lantean.QBTMud.Components
allAreSuperSeeding = false;
}
if (MajorVersion < 5)
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
if (torrent.State != "pausedUP" && torrent.State != "pausedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
allAreStopped = false;
}
else
{
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
thereAreStopped = true;
}
if (!torrent.ForceStart)
@@ -532,7 +498,7 @@ namespace Lantean.QBTMud.Components
actionStates["superSeeding"] = ActionState.Hidden;
}
if (allArePaused)
if (allAreStopped)
{
actionStates["pause"] = ActionState.Hidden;
}
@@ -540,13 +506,11 @@ namespace Lantean.QBTMud.Components
{
actionStates["forceStart"] = ActionState.Hidden;
}
else if (!thereArePaused && !thereAreForceStart)
else if (!thereAreStopped && !thereAreForceStart)
{
actionStates["start"] = ActionState.Hidden;
}
if (MajorVersion >= 5)
{
if (actionStates.TryGetValue("start", out ActionState? startActionState))
{
startActionState.TextOverride = "Start";
@@ -564,7 +528,6 @@ namespace Lantean.QBTMud.Components
{
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
}
}
if (!allAreAutoTmm && thereAreAutoTmm)
{
@@ -706,4 +669,4 @@ namespace Lantean.QBTMud.Components
MenuItems,
}
}
}

View File

@@ -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>

View File

@@ -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)
@@ -169,7 +171,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.AddTrackersToTorrent(Hash, trackers);
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
}
protected Task EditTrackerToolbar()
@@ -209,7 +211,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
}
protected Task CopyTrackerUrlToolbar()
@@ -301,4 +303,4 @@ namespace Lantean.QBTMud.Components
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -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" />

View File

@@ -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);
}
}
}

View File

@@ -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" />

View File

@@ -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 =>

View File

@@ -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)

View File

@@ -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>
</td>

View File

@@ -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>

View File

@@ -3,6 +3,7 @@ using Lantean.QBTMud.Components.Dialogs;
using Lantean.QBTMud.Filter;
using Lantean.QBTMud.Models;
using MudBlazor;
using System.Linq;
namespace Lantean.QBTMud.Helpers
{
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Torrents = files;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
foreach (var stream in streams)
{
@@ -74,15 +75,10 @@ namespace Lantean.QBTMud.Helpers
{
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
}
if (string.IsNullOrEmpty(options.Cookie))
{
addTorrentParams.Cookie = options.Cookie;
}
addTorrentParams.DownloadLimit = options.DownloadLimit;
addTorrentParams.DownloadPath = options.DownloadPath;
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
addTorrentParams.Paused = !options.StartTorrent;
addTorrentParams.RatioLimit = options.RatioLimit;
addTorrentParams.RenameTorrent = options.RenameTorrent;
addTorrentParams.SavePath = options.SavePath;
@@ -123,7 +119,7 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Urls = options.Urls;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
}
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
@@ -243,7 +239,7 @@ namespace Lantean.QBTMud.Helpers
var shareRatio = (ShareRatio)dialogResult.Data;
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, null, torrents.Select(t => t.Hash).ToArray());
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, hashes: torrents.Select(t => t.Hash).ToArray());
}
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
@@ -436,4 +432,4 @@ namespace Lantean.QBTMud.Helpers
await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
}
}
}
}

View File

@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
{
if (seconds is null)
{
return "";
return string.Empty;
}
if (seconds == 8640000)
const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
var value = seconds.Value;
if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
{
return "∞";
}
if (seconds < 60)
if (value <= 0)
{
return "< 1m";
}
TimeSpan time;
try
var time = TimeSpan.FromSeconds(value);
if (time.TotalMinutes < 1)
{
time = TimeSpan.FromSeconds(seconds.Value);
}
catch
{
return "∞";
return "< 1m";
}
var sb = new StringBuilder();
if (prefix is not null)
{
@@ -404,8 +404,6 @@ namespace Lantean.QBTMud.Helpers
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
Status.Resumed => (Icons.Material.Filled.PlayArrow, Color.Success),
Status.Paused => (Icons.Material.Filled.Pause, Color.Default),
Status.Stopped => (Icons.Material.Filled.Stop, Color.Default),
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
@@ -418,4 +416,4 @@ namespace Lantean.QBTMud.Helpers
};
}
}
}
}

View 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,
};
}
}
}

View File

@@ -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)
@@ -199,15 +200,8 @@ namespace Lantean.QBTMud.Helpers
break;
case Status.Resumed:
if (!state.Contains("resumed"))
{
return false;
}
break;
case Status.Paused:
if (!state.Contains("paused") || !state.Contains("stopped"))
case Status.Stopped:
if (state != "stoppedDL" && state != "stoppedUP")
{
return false;
}
@@ -284,4 +278,4 @@ namespace Lantean.QBTMud.Helpers
};
}
}
}
}

View File

@@ -1,34 +0,0 @@

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;
}
}
}

View File

@@ -12,10 +12,10 @@
<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.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="MudBlazor" Version="8.7.0" />
<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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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, Version);
MainData = DataManager.CreateMainData(data);
MarkTorrentsDirty();
_requestId = data.ResponseId;
_refreshInterval = MainData.ServerState.RefreshInterval;
@@ -126,32 +141,51 @@ namespace Lantean.QBTMud.Layout
return;
}
var shouldRender = false;
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data, Version);
MainData = DataManager.CreateMainData(data);
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)
@@ -188,4 +291,4 @@ namespace Lantean.QBTMud.Layout
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -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>

View File

@@ -11,8 +11,7 @@
Dictionary<string, HashSet<string>> tagState,
Dictionary<string, HashSet<string>> categoriesState,
Dictionary<string, HashSet<string>> statusState,
Dictionary<string, HashSet<string>> trackersState,
int majorVersion)
Dictionary<string, HashSet<string>> trackersState)
{
Torrents = torrents.ToDictionary();
Tags = tags.ToHashSet();
@@ -23,7 +22,6 @@
CategoriesState = categoriesState;
StatusState = statusState;
TrackersState = trackersState;
MajorVersion = majorVersion;
}
public Dictionary<string, Torrent> Torrents { get; }
@@ -38,6 +36,5 @@
public Dictionary<string, HashSet<string>> TrackersState { get; }
public string? SelectedTorrentHash { get; set; }
public bool LostConnection { get; set; }
public int MajorVersion { get; }
}
}
}

View File

@@ -6,7 +6,6 @@
Downloading,
Seeding,
Completed,
Resumed,
Paused,
Stopped,
Active,
@@ -16,6 +15,5 @@
StalledDownloading,
Checking,
Errored,
}
}
}

View File

@@ -1,6 +1,4 @@
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMud.Models
namespace Lantean.QBTMud.Models
{
public record TorrentOptions
{

View File

@@ -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">
@@ -1104,4 +1108,6 @@
<MudText Typo="Typo.body1" Class="py-1">The free IP to Country Lite database by DB-IP is used for resolving the countries of peers. The database is licensed under the Creative Commons Attribution 4.0 International License (<MudLink Target="https://db-ip.com/" Href="https://db-ip.com/" rel="noopener ">https://db-ip.com/</MudLink>)</MudText>
</MudContainer>
</MudTabPanel>
</MudTabs>
</MudTabs>
</div>
</div>

View File

@@ -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>

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
}
@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>
<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>

View File

@@ -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>

View File

@@ -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

View File

@@ -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
{

View File

@@ -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);

View File

@@ -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

View File

@@ -4,11 +4,11 @@ namespace Lantean.QBTMud.Services
{
public interface IDataManager
{
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
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,10 +16,10 @@ 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);
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
}
}
}

View File

@@ -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);
@@ -255,4 +313,117 @@ 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;
}

View File

@@ -37,4 +37,4 @@
<script src="./js/interop.js"></script>
</body>
</html>
</html>

View File

@@ -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);

View File

@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.2" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,11 @@
namespace Lantean.QBitTorrentClient.Test
{
public class UnitTest1
{
[Fact]
public void Test1()
{
}
}
}

View File

@@ -1,4 +1,9 @@
using Lantean.QBitTorrentClient.Models;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http.Json;
using System.Text.Json;
@@ -105,6 +110,8 @@ namespace Lantean.QBitTorrentClient
public async Task SetApplicationPreferences(UpdatePreferences preferences)
{
preferences.Validate();
var json = JsonSerializer.Serialize(preferences, _options);
var content = new FormUrlEncodedBuilder()
@@ -116,6 +123,49 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies()
{
var response = await _httpClient.GetAsync("app/cookies");
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<ApplicationCookie>(response.Content);
}
public async Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies)
{
var json = JsonSerializer.Serialize(cookies, _options);
var content = new FormUrlEncodedBuilder()
.Add("cookies", json)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("app/setCookies", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<string> RotateApiKey()
{
var response = await _httpClient.PostAsync("app/rotateAPIKey", null);
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("apiKey", out var apiKeyElement))
{
return apiKeyElement.GetString() ?? string.Empty;
}
return string.Empty;
}
public async Task<string> GetDefaultSavePath()
{
var response = await _httpClient.GetAsync("app/defaultSavePath");
@@ -145,6 +195,43 @@ namespace Lantean.QBitTorrentClient
#endregion Application
#region Client data
public async Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null)
{
HttpResponseMessage response;
if (keys is null)
{
response = await _httpClient.GetAsync("clientdata/load");
}
else
{
var query = new QueryBuilder()
.Add("keys", JsonSerializer.Serialize(keys, _options));
response = await _httpClient.GetAsync("clientdata/load", query);
}
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonDictionary<string, JsonElement>(response.Content);
}
public async Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data)
{
var json = JsonSerializer.Serialize(data, _options);
var content = new FormUrlEncodedBuilder()
.Add("data", json)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("clientdata/store", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
#endregion Client data
#region Log
public async Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null)
@@ -305,7 +392,7 @@ namespace Lantean.QBitTorrentClient
#region Torrent management
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes)
public async Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes)
{
var query = new QueryBuilder();
if (filter is not null)
@@ -344,6 +431,10 @@ namespace Lantean.QBitTorrentClient
{
query.Add("private", isPrivate.Value ? "true" : "false");
}
if (includeFiles is not null)
{
query.Add("includeFiles", includeFiles.Value ? "true" : "false");
}
var response = await _httpClient.GetAsync("torrents/info", query);
@@ -379,6 +470,43 @@ namespace Lantean.QBitTorrentClient
return await GetJsonList<WebSeed>(response.Content);
}
public async Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("urls", string.Join('|', urls))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/addWebSeeds", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("origUrl", originalUrl)
.Add("newUrl", newUrl)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/editWebSeed", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls)
{
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("urls", string.Join('|', urls))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/removeWebSeeds", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes)
{
var query = new QueryBuilder();
@@ -411,18 +539,6 @@ namespace Lantean.QBitTorrentClient
return await GetJsonList<string>(response.Content);
}
public async Task PauseTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/pause", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task StopTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -433,18 +549,6 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task ResumeTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/resume", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task StartTorrents(bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -479,10 +583,11 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task ReannounceTorrents(bool? all = null, params string[] hashes)
public async Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.AddIfNotNullOrEmpty("urls", trackers is null ? null : string.Join('|', trackers))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/reannounce", content);
@@ -490,13 +595,15 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task AddTorrent(AddTorrentParams addTorrentParams)
public async Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams)
{
var content = new MultipartFormDataContent();
if (addTorrentParams.Urls is not null)
if (addTorrentParams.Urls?.Any() == true)
{
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
}
if (addTorrentParams.Torrents is not null)
{
foreach (var (name, stream) in addTorrentParams.Torrents)
@@ -504,6 +611,7 @@ namespace Lantean.QBitTorrentClient
content.Add(new StreamContent(stream), "torrents", name);
}
}
if (addTorrentParams.SkipChecking is not null)
{
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
@@ -520,12 +628,10 @@ namespace Lantean.QBitTorrentClient
{
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
}
// v4
if (addTorrentParams.Paused is not null)
if (addTorrentParams.Forced is not null)
{
content.AddString("paused", addTorrentParams.Paused.Value);
content.AddString("forced", addTorrentParams.Forced.Value);
}
// v5
if (addTorrentParams.Stopped is not null)
{
content.AddString("stopped", addTorrentParams.Stopped.Value);
@@ -590,21 +696,61 @@ namespace Lantean.QBitTorrentClient
{
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
}
if (addTorrentParams.Cookie is not null)
if (addTorrentParams.Downloader is not null)
{
content.AddString("cookie", addTorrentParams.Cookie);
content.AddString("downloader", addTorrentParams.Downloader);
}
if (addTorrentParams.FilePriorities is not null)
{
var priorities = string.Join(',', addTorrentParams.FilePriorities.Select(priority => ((int)priority).ToString(CultureInfo.InvariantCulture)));
content.AddString("filePriorities", priorities);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslCertificate))
{
content.AddString("ssl_certificate", addTorrentParams.SslCertificate!);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslPrivateKey))
{
content.AddString("ssl_private_key", addTorrentParams.SslPrivateKey!);
}
if (!string.IsNullOrWhiteSpace(addTorrentParams.SslDhParams))
{
content.AddString("ssl_dh_params", addTorrentParams.SslDhParams!);
}
var response = await _httpClient.PostAsync("torrents/add", content);
if (response.StatusCode == HttpStatusCode.Conflict)
{
var conflictMessage = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(conflictMessage))
{
conflictMessage = "All torrents failed to add.";
}
throw new HttpRequestException(conflictMessage, null, response.StatusCode);
}
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return new AddTorrentResult(0, 0, 0, Array.Empty<string>());
}
return JsonSerializer.Deserialize<AddTorrentResult>(payload, _options) ?? new AddTorrentResult(0, 0, 0, Array.Empty<string>());
}
public async Task AddTrackersToTorrent(string hash, IEnumerable<string> urls)
public async Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes)
{
if (all is not true && (hashes is null || hashes.Length == 0))
{
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
.Add("urls", string.Join('\n', urls))
.ToFormUrlEncodedContent();
@@ -613,23 +759,42 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task EditTracker(string hash, string originalUrl, string newUrl)
public async Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null)
{
if ((newUrl is null) && (tier is null))
{
throw new ArgumentException("Must specify at least one of newUrl or tier.");
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.Add("originalUrl", originalUrl)
.Add("newUrl", newUrl)
.ToFormUrlEncodedContent();
.Add("url", url);
var response = await _httpClient.PostAsync("torrents/editTracker", content);
if (!string.IsNullOrEmpty(newUrl))
{
content.Add("newUrl", newUrl!);
}
if (tier is not null)
{
content.Add("tier", tier.Value);
}
var form = content.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/editTracker", form);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task RemoveTrackers(string hash, IEnumerable<string> urls)
public async Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes)
{
if (all is not true && (hashes is null || hashes.Length == 0))
{
throw new ArgumentException("Specify at least one torrent hash or set all=true.", nameof(hashes));
}
var content = new FormUrlEncodedBuilder()
.Add("hash", hash)
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
.AddPipeSeparated("urls", urls)
.ToFormUrlEncodedContent();
@@ -732,13 +897,14 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes)
public async Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes)
.Add("ratioLimit", ratioLimit)
.Add("seedingTimeLimit", seedingTimeLimit)
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
.AddIfNotNullOrEmpty("shareLimitAction", shareLimitAction)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
@@ -795,6 +961,18 @@ namespace Lantean.QBitTorrentClient
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentComment(IEnumerable<string> hashes, string comment)
{
var content = new FormUrlEncodedBuilder()
.Add("hashes", string.Join('|', hashes))
.Add("comment", comment)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/setComment", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes)
{
var content = new FormUrlEncodedBuilder()
@@ -995,8 +1173,180 @@ namespace Lantean.QBitTorrentClient
return Task.FromResult($"{_httpClient.BaseAddress}torrents/export?hash={hash}");
}
public async Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null)
{
var builder = new FormUrlEncodedBuilder()
.Add("source", source);
if (!string.IsNullOrWhiteSpace(downloader))
{
builder.Add("downloader", downloader!);
}
var response = await _httpClient.PostAsync("torrents/fetchMetadata", builder.ToFormUrlEncodedContent());
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return null;
}
return JsonSerializer.Deserialize<TorrentMetadata>(payload, _options);
}
public async Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents)
{
var content = new MultipartFormDataContent();
foreach (var (fileName, stream) in torrents)
{
content.Add(new StreamContent(stream), "torrents", fileName);
}
var response = await _httpClient.PostAsync("torrents/parseMetadata", content);
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<TorrentMetadata>(response.Content);
}
public async Task<byte[]> SaveMetadata(string source)
{
var content = new FormUrlEncodedBuilder()
.Add("source", source)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/saveMetadata", content);
await ThrowIfNotSuccessfulStatusCode(response);
return await response.Content.ReadAsByteArrayAsync();
}
#endregion Torrent management
#region Torrent creator
public async Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request)
{
if (request is null)
throw new ArgumentNullException(nameof(request));
if (string.IsNullOrWhiteSpace(request.SourcePath))
throw new ArgumentException("SourcePath is required.", nameof(request));
var builder = new FormUrlEncodedBuilder()
.Add("sourcePath", request.SourcePath);
if (!string.IsNullOrWhiteSpace(request.TorrentFilePath))
{
builder.Add("torrentFilePath", request.TorrentFilePath!);
}
if (request.PieceSize.HasValue)
{
builder.Add("pieceSize", request.PieceSize.Value);
}
if (request.Private.HasValue)
{
builder.Add("private", request.Private.Value);
}
if (request.StartSeeding.HasValue)
{
builder.Add("startSeeding", request.StartSeeding.Value);
}
if (!string.IsNullOrWhiteSpace(request.Comment))
{
builder.Add("comment", request.Comment!);
}
if (!string.IsNullOrWhiteSpace(request.Source))
{
builder.Add("source", request.Source!);
}
if (request.Trackers is not null)
{
builder.Add("trackers", string.Join('|', request.Trackers));
}
if (request.UrlSeeds is not null)
{
builder.Add("urlSeeds", string.Join('|', request.UrlSeeds));
}
if (!string.IsNullOrWhiteSpace(request.Format))
{
builder.Add("format", request.Format!);
}
if (request.OptimizeAlignment.HasValue)
{
builder.Add("optimizeAlignment", request.OptimizeAlignment.Value);
}
if (request.PaddedFileSizeLimit.HasValue)
{
builder.Add("paddedFileSizeLimit", request.PaddedFileSizeLimit.Value);
}
var response = await _httpClient.PostAsync("torrentcreator/addTask", builder.ToFormUrlEncodedContent());
await ThrowIfNotSuccessfulStatusCode(response);
var payload = await response.Content.ReadAsStringAsync();
if (string.IsNullOrWhiteSpace(payload))
{
return string.Empty;
}
var json = JsonSerializer.Deserialize<JsonElement>(payload, _options);
if (json.ValueKind == JsonValueKind.Object && json.TryGetProperty("taskID", out var idElement))
{
return idElement.GetString() ?? string.Empty;
}
return string.Empty;
}
public async Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null)
{
HttpResponseMessage response;
if (string.IsNullOrWhiteSpace(taskId))
{
response = await _httpClient.GetAsync("torrentcreator/status");
}
else
{
var query = new QueryBuilder()
.Add("taskID", taskId);
response = await _httpClient.GetAsync("torrentcreator/status", query);
}
await ThrowIfNotSuccessfulStatusCode(response);
return await GetJsonList<TorrentCreationTaskStatus>(response.Content);
}
public async Task<byte[]> GetTorrentCreationTaskFile(string taskId)
{
var query = new QueryBuilder()
.Add("taskID", taskId);
var response = await _httpClient.GetAsync("torrentcreator/torrentFile", query);
await ThrowIfNotSuccessfulStatusCode(response);
return await response.Content.ReadAsByteArrayAsync();
}
public async Task DeleteTorrentCreationTask(string taskId)
{
var content = new FormUrlEncodedBuilder()
.Add("taskID", taskId)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrentcreator/deleteTask", content);
await ThrowIfNotSuccessfulStatusCode(response);
}
#endregion Torrent creator
#region RSS
public async Task AddRssFolder(string path)
@@ -1334,4 +1684,4 @@ namespace Lantean.QBitTorrentClient
throw new HttpRequestException(errorMessage, null, response.StatusCode);
}
}
}
}

View File

@@ -1,24 +1,10 @@
using Lantean.QBitTorrentClient.Models;
using System.Linq;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBitTorrentClient
{
public static class ApiClientExtensions
{
public static Task PauseTorrent(this IApiClient apiClient, string hash)
{
return apiClient.PauseTorrents(null, hash);
}
public static Task PauseTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
{
return apiClient.PauseTorrents(null, hashes.ToArray());
}
public static Task PauseAllTorrents(this IApiClient apiClient)
{
return apiClient.PauseTorrents(true);
}
public static Task StopTorrent(this IApiClient apiClient, string hash)
{
return apiClient.StopTorrents(null, hash);
@@ -34,21 +20,6 @@ namespace Lantean.QBitTorrentClient
return apiClient.StopTorrents(true);
}
public static Task ResumeTorrent(this IApiClient apiClient, string hash)
{
return apiClient.ResumeTorrents(null, hash);
}
public static Task ResumeTorrents(this IApiClient apiClient, IEnumerable<string> hashes)
{
return apiClient.ResumeTorrents(null, hashes.ToArray());
}
public static Task ResumeAllTorrents(this IApiClient apiClient)
{
return apiClient.ResumeTorrents(true);
}
public static Task StartTorrent(this IApiClient apiClient, string hash)
{
return apiClient.StartTorrents(null, hash);
@@ -158,7 +129,7 @@ namespace Lantean.QBitTorrentClient
public static Task ReannounceTorrent(this IApiClient apiClient, string hash)
{
return apiClient.ReannounceTorrents(null, hash);
return apiClient.ReannounceTorrents(null, null, hash);
}
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
@@ -189,4 +160,4 @@ namespace Lantean.QBitTorrentClient
return unusedTags;
}
}
}
}

View File

@@ -27,7 +27,7 @@ namespace Lantean.QBitTorrentClient.Converters
{
writer.WriteNumberValue(0);
}
else if (value.IsDefaltFolder)
else if (value.IsDefaultFolder)
{
writer.WriteNumberValue(1);
}

View File

@@ -1,4 +1,8 @@
using Lantean.QBitTorrentClient.Models;
using System.Collections.Generic;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBitTorrentClient
{
@@ -28,6 +32,12 @@ namespace Lantean.QBitTorrentClient
Task SetApplicationPreferences(UpdatePreferences preferences);
Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies();
Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies);
Task<string> RotateApiKey();
Task<string> GetDefaultSavePath();
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
@@ -36,6 +46,14 @@ namespace Lantean.QBitTorrentClient
#endregion Application
#region Client data
Task<IReadOnlyDictionary<string, JsonElement>> LoadClientData(IEnumerable<string>? keys = null);
Task StoreClientData(IReadOnlyDictionary<string, JsonElement> data);
#endregion Client data
#region Log
Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null);
@@ -74,7 +92,7 @@ namespace Lantean.QBitTorrentClient
#region Torrent management
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, params string[] hashes);
Task<IReadOnlyList<Torrent>> GetTorrentList(string? filter = null, string? category = null, string? tag = null, string? sort = null, bool? reverse = null, int? limit = null, int? offset = null, bool? isPrivate = null, bool? includeFiles = null, params string[] hashes);
Task<TorrentProperties> GetTorrentProperties(string hash);
@@ -82,16 +100,18 @@ namespace Lantean.QBitTorrentClient
Task<IReadOnlyList<WebSeed>> GetTorrentWebSeeds(string hash);
Task AddTorrentWebSeeds(string hash, IEnumerable<string> urls);
Task EditTorrentWebSeed(string hash, string originalUrl, string newUrl);
Task RemoveTorrentWebSeeds(string hash, IEnumerable<string> urls);
Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes);
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
Task<IReadOnlyList<string>> GetTorrentPieceHashes(string hash);
Task PauseTorrents(bool? all = null, params string[] hashes);
Task ResumeTorrents(bool? all = null, params string[] hashes);
Task StartTorrents(bool? all = null, params string[] hashes);
Task StopTorrents(bool? all = null, params string[] hashes);
@@ -100,15 +120,15 @@ namespace Lantean.QBitTorrentClient
Task RecheckTorrents(bool? all = null, params string[] hashes);
Task ReannounceTorrents(bool? all = null, params string[] hashes);
Task ReannounceTorrents(bool? all = null, IEnumerable<string>? trackers = null, params string[] hashes);
Task AddTorrent(AddTorrentParams addTorrentParams);
Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams);
Task AddTrackersToTorrent(string hash, IEnumerable<string> urls);
Task AddTrackersToTorrent(IEnumerable<string> urls, bool? all = null, params string[] hashes);
Task EditTracker(string hash, string originalUrl, string newUrl);
Task EditTracker(string hash, string url, string? newUrl = null, int? tier = null);
Task RemoveTrackers(string hash, IEnumerable<string> urls);
Task RemoveTrackers(IEnumerable<string> urls, bool? all = null, params string[] hashes);
Task AddPeers(IEnumerable<string> hashes, IEnumerable<PeerId> peers);
@@ -126,7 +146,7 @@ namespace Lantean.QBitTorrentClient
Task SetTorrentDownloadLimit(long limit, bool? all = null, params string[] hashes);
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, bool? all = null, params string[] hashes);
Task SetTorrentShareLimit(float ratioLimit, float seedingTimeLimit, float inactiveSeedingTimeLimit, ShareLimitAction? shareLimitAction = null, bool? all = null, params string[] hashes);
Task<IReadOnlyDictionary<string, long>> GetTorrentUploadLimit(bool? all = null, params string[] hashes);
@@ -136,6 +156,8 @@ namespace Lantean.QBitTorrentClient
Task SetTorrentName(string name, string hash);
Task SetTorrentComment(IEnumerable<string> hashes, string comment);
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
@@ -172,8 +194,26 @@ namespace Lantean.QBitTorrentClient
Task<string> GetExportUrl(string hash);
Task<TorrentMetadata?> FetchMetadata(string source, string? downloader = null);
Task<IReadOnlyList<TorrentMetadata>> ParseMetadata(IEnumerable<(string FileName, Stream Content)> torrents);
Task<byte[]> SaveMetadata(string source);
#endregion Torrent management
#region Torrent creator
Task<string> AddTorrentCreationTask(TorrentCreationTaskRequest request);
Task<IReadOnlyList<TorrentCreationTaskStatus>> GetTorrentCreationTasks(string? taskId = null);
Task<byte[]> GetTorrentCreationTaskFile(string taskId);
Task DeleteTorrentCreationTask(string taskId);
#endregion Torrent creator
#region RSS
Task AddRssFolder(string path);
@@ -230,4 +270,4 @@ namespace Lantean.QBitTorrentClient
#endregion Search
}
}
}

View File

@@ -12,9 +12,8 @@
public bool? AddToTopOfQueue { get; set; }
// v4
public bool? Paused { get; set; }
// v5
public bool? Forced { get; set; }
public bool? Stopped { get; set; }
public string? SavePath { get; set; }
@@ -24,13 +23,13 @@
public bool? UseDownloadPath { get; set; }
public string? Category { get; set; }
public IEnumerable<string>? Tags { get; set; }
public string? RenameTorrent { get; set; }
public long? UploadLimit { get; set; }
public long? DownloadLimit { get; set; }
public float? RatioLimit { get; set; }
@@ -47,8 +46,16 @@
public TorrentContentLayout? ContentLayout { get; set; }
public string? Cookie { get; set; }
public IEnumerable<Priority>? FilePriorities { get; set; }
public string? Downloader { get; set; }
public string? SslCertificate { get; set; }
public string? SslPrivateKey { get; set; }
public string? SslDhParams { get; set; }
public Dictionary<string, Stream>? Torrents { get; set; }
}
}
}

View File

@@ -0,0 +1,30 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record AddTorrentResult
{
[JsonConstructor]
public AddTorrentResult(int successCount, int failureCount, int pendingCount, IReadOnlyList<string>? addedTorrentIds)
{
SuccessCount = successCount;
FailureCount = failureCount;
PendingCount = pendingCount;
AddedTorrentIds = addedTorrentIds ?? Array.Empty<string>();
}
[JsonPropertyName("success_count")]
public int SuccessCount { get; }
[JsonPropertyName("failure_count")]
public int FailureCount { get; }
[JsonPropertyName("pending_count")]
public int PendingCount { get; }
[JsonPropertyName("added_torrent_ids")]
public IReadOnlyList<string> AddedTorrentIds { get; }
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record ApplicationCookie
{
[JsonConstructor]
public ApplicationCookie(string name, string? domain, string? path, string? value, long? expirationDate)
{
Name = name;
Domain = domain;
Path = path;
Value = value;
ExpirationDate = expirationDate;
}
[JsonPropertyName("name")]
public string Name { get; }
[JsonPropertyName("domain")]
public string? Domain { get; }
[JsonPropertyName("path")]
public string? Path { get; }
[JsonPropertyName("value")]
public string? Value { get; }
[JsonPropertyName("expirationDate")]
public long? ExpirationDate { get; }
}
}

View File

@@ -15,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
IReadOnlyList<string>? tags,
IReadOnlyList<string>? tagsRemoved,
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
IReadOnlyList<string>? trackersRemoved,
ServerState? serverState)
{
ResponseId = responseId;
@@ -26,6 +27,7 @@ namespace Lantean.QBitTorrentClient.Models
Tags = tags;
TagsRemoved = tagsRemoved;
Trackers = trackers;
TrackersRemoved = trackersRemoved;
ServerState = serverState;
}
@@ -62,4 +64,4 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("server_state")]
public ServerState? ServerState { get; }
}
}
}

View File

@@ -16,6 +16,7 @@ namespace Lantean.QBitTorrentClient.Models
string? flags,
string? flagsDescription,
string? iPAddress,
string? i2pDestination,
string? clientId,
int? port,
float? progress,
@@ -33,6 +34,7 @@ namespace Lantean.QBitTorrentClient.Models
Flags = flags;
FlagsDescription = flagsDescription;
IPAddress = iPAddress;
I2pDestination = i2pDestination;
ClientId = clientId;
Port = port;
Progress = progress;
@@ -71,6 +73,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("ip")]
public string? IPAddress { get; }
[JsonPropertyName("i2p_dest")]
public string? I2pDestination { get; }
[JsonPropertyName("peer_id_client")]
public string? ClientId { get; }
@@ -89,4 +94,4 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("uploaded")]
public long? Uploaded { get; }
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Lantean.QBitTorrentClient.Models
[JsonConstructor]
public Preferences(
bool addToTopOfQueue,
bool addStoppedEnabled,
string addTrackers,
bool addTrackersEnabled,
int altDlLimit,
@@ -14,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
bool alternativeWebuiEnabled,
string alternativeWebuiPath,
string announceIp,
int announcePort,
bool announceToAllTiers,
bool announceToAllTrackers,
bool anonymousMode,
@@ -85,6 +87,7 @@ namespace Lantean.QBitTorrentClient.Models
int i2pPort,
bool idnSupportEnabled,
bool incompleteFilesExt,
bool useUnwantedFolder,
bool ipFilterEnabled,
string ipFilterPath,
bool ipFilterTrackers,
@@ -92,6 +95,8 @@ namespace Lantean.QBitTorrentClient.Models
bool limitTcpOverhead,
bool limitUtpRate,
int listenPort,
bool sslEnabled,
int sslListenPort,
string locale,
bool lsd,
bool mailNotificationAuthEnabled,
@@ -112,7 +117,7 @@ namespace Lantean.QBitTorrentClient.Models
int maxConnecPerTorrent,
int maxInactiveSeedingTime,
bool maxInactiveSeedingTimeEnabled,
int maxRatio,
float maxRatio,
int maxRatioAct,
bool maxRatioEnabled,
int maxSeedingTime,
@@ -160,6 +165,7 @@ namespace Lantean.QBitTorrentClient.Models
string savePath,
bool savePathChangedTmmEnabled,
int saveResumeDataInterval,
int saveStatisticsInterval,
Dictionary<string, SaveLocation> scanDirs,
int scheduleFromHour,
int scheduleFromMin,
@@ -177,12 +183,12 @@ namespace Lantean.QBitTorrentClient.Models
int socketReceiveBufferSize,
int socketSendBufferSize,
bool ssrfMitigation,
bool startPausedEnabled,
int stopTrackerTimeout,
string tempPath,
bool tempPathEnabled,
bool torrentChangedTmmEnabled,
string torrentContentLayout,
string torrentContentRemoveOption,
int torrentFileSizeLimit,
string torrentStopCondition,
int upLimit,
@@ -192,10 +198,12 @@ namespace Lantean.QBitTorrentClient.Models
int upnpLeaseDuration,
bool useCategoryPathsInManualMode,
bool useHttps,
bool ignoreSslErrors,
bool useSubcategories,
int utpTcpMixedMode,
bool validateHttpsTrackerCertificate,
string webUiAddress,
string webUiApiKey,
int webUiBanDuration,
bool webUiClickjackingProtectionEnabled,
bool webUiCsrfProtectionEnabled,
@@ -217,6 +225,7 @@ namespace Lantean.QBitTorrentClient.Models
)
{
AddToTopOfQueue = addToTopOfQueue;
AddStoppedEnabled = addStoppedEnabled;
AddTrackers = addTrackers;
AddTrackersEnabled = addTrackersEnabled;
AltDlLimit = altDlLimit;
@@ -224,6 +233,7 @@ namespace Lantean.QBitTorrentClient.Models
AlternativeWebuiEnabled = alternativeWebuiEnabled;
AlternativeWebuiPath = alternativeWebuiPath;
AnnounceIp = announceIp;
AnnouncePort = announcePort;
AnnounceToAllTiers = announceToAllTiers;
AnnounceToAllTrackers = announceToAllTrackers;
AnonymousMode = anonymousMode;
@@ -295,6 +305,7 @@ namespace Lantean.QBitTorrentClient.Models
I2pPort = i2pPort;
IdnSupportEnabled = idnSupportEnabled;
IncompleteFilesExt = incompleteFilesExt;
UseUnwantedFolder = useUnwantedFolder;
IpFilterEnabled = ipFilterEnabled;
IpFilterPath = ipFilterPath;
IpFilterTrackers = ipFilterTrackers;
@@ -302,6 +313,8 @@ namespace Lantean.QBitTorrentClient.Models
LimitTcpOverhead = limitTcpOverhead;
LimitUtpRate = limitUtpRate;
ListenPort = listenPort;
SslEnabled = sslEnabled;
SslListenPort = sslListenPort;
Locale = locale;
Lsd = lsd;
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
@@ -370,6 +383,7 @@ namespace Lantean.QBitTorrentClient.Models
SavePath = savePath;
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
SaveResumeDataInterval = saveResumeDataInterval;
SaveStatisticsInterval = saveStatisticsInterval;
ScanDirs = scanDirs;
ScheduleFromHour = scheduleFromHour;
ScheduleFromMin = scheduleFromMin;
@@ -387,12 +401,12 @@ namespace Lantean.QBitTorrentClient.Models
SocketReceiveBufferSize = socketReceiveBufferSize;
SocketSendBufferSize = socketSendBufferSize;
SsrfMitigation = ssrfMitigation;
StartPausedEnabled = startPausedEnabled;
StopTrackerTimeout = stopTrackerTimeout;
TempPath = tempPath;
TempPathEnabled = tempPathEnabled;
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
TorrentContentLayout = torrentContentLayout;
TorrentContentRemoveOption = torrentContentRemoveOption;
TorrentFileSizeLimit = torrentFileSizeLimit;
TorrentStopCondition = torrentStopCondition;
UpLimit = upLimit;
@@ -402,10 +416,12 @@ namespace Lantean.QBitTorrentClient.Models
UpnpLeaseDuration = upnpLeaseDuration;
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
UseHttps = useHttps;
IgnoreSslErrors = ignoreSslErrors;
UseSubcategories = useSubcategories;
UtpTcpMixedMode = utpTcpMixedMode;
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
WebUiAddress = webUiAddress;
WebUiApiKey = webUiApiKey;
WebUiBanDuration = webUiBanDuration;
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
@@ -429,6 +445,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("add_to_top_of_queue")]
public bool AddToTopOfQueue { get; }
[JsonPropertyName("add_stopped_enabled")]
public bool AddStoppedEnabled { get; }
[JsonPropertyName("add_trackers")]
public string AddTrackers { get; }
@@ -450,6 +469,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("announce_ip")]
public string AnnounceIp { get; }
[JsonPropertyName("announce_port")]
public int AnnouncePort { get; }
[JsonPropertyName("announce_to_all_tiers")]
public bool AnnounceToAllTiers { get; }
@@ -663,6 +685,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("incomplete_files_ext")]
public bool IncompleteFilesExt { get; }
[JsonPropertyName("use_unwanted_folder")]
public bool UseUnwantedFolder { get; }
[JsonPropertyName("ip_filter_enabled")]
public bool IpFilterEnabled { get; }
@@ -684,6 +709,12 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("listen_port")]
public int ListenPort { get; }
[JsonPropertyName("ssl_enabled")]
public bool SslEnabled { get; }
[JsonPropertyName("ssl_listen_port")]
public int SslListenPort { get; }
[JsonPropertyName("locale")]
public string Locale { get; }
@@ -745,7 +776,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; }
@@ -888,6 +919,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("save_resume_data_interval")]
public int SaveResumeDataInterval { get; }
[JsonPropertyName("save_statistics_interval")]
public int SaveStatisticsInterval { get; }
[JsonPropertyName("scan_dirs")]
public Dictionary<string, SaveLocation> ScanDirs { get; }
@@ -939,9 +973,6 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("ssrf_mitigation")]
public bool SsrfMitigation { get; }
[JsonPropertyName("start_paused_enabled")]
public bool StartPausedEnabled { get; }
[JsonPropertyName("stop_tracker_timeout")]
public int StopTrackerTimeout { get; }
@@ -957,6 +988,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("torrent_content_layout")]
public string TorrentContentLayout { get; }
[JsonPropertyName("torrent_content_remove_option")]
public string TorrentContentRemoveOption { get; }
[JsonPropertyName("torrent_file_size_limit")]
public int TorrentFileSizeLimit { get; }
@@ -984,6 +1018,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("use_https")]
public bool UseHttps { get; }
[JsonPropertyName("ignore_ssl_errors")]
public bool IgnoreSslErrors { get; }
[JsonPropertyName("use_subcategories")]
public bool UseSubcategories { get; }
@@ -996,6 +1033,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_address")]
public string WebUiAddress { get; }
[JsonPropertyName("web_ui_api_key")]
public string WebUiApiKey { get; }
[JsonPropertyName("web_ui_ban_duration")]
public int WebUiBanDuration { get; }
@@ -1050,4 +1090,4 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("web_ui_password")]
public string WebUiPassword { get; }
}
}
}

View File

@@ -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;
}

View File

@@ -1,264 +1,219 @@
using Lantean.QBitTorrentClient.Converters;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record Torrent
{
[JsonConstructor]
public Torrent(
long? addedOn,
long? amountLeft,
bool? automaticTorrentManagement,
float? availability,
string? category,
long? completed,
long? completionOn,
string? contentPath,
long? downloadLimit,
long? downloadSpeed,
long? downloaded,
long? downloadedSession,
long? estimatedTimeOfArrival,
bool? firstLastPiecePriority,
bool? forceStart,
string hash,
string? infoHashV1,
string? infoHashV2,
long? lastActivity,
string? magnetUri,
float? maxRatio,
int? maxSeedingTime,
string? name,
int? numberComplete,
int? numberIncomplete,
int? numberLeeches,
int? numberSeeds,
int? priority,
float? progress,
float? ratio,
float? ratioLimit,
string? savePath,
long? seedingTime,
int? seedingTimeLimit,
long? seenComplete,
bool? sequentialDownload,
long? size,
string? state,
bool? superSeeding,
IReadOnlyList<string>? tags,
int? timeActive,
long? totalSize,
string? tracker,
long? uploadLimit,
long? uploaded,
long? uploadedSession,
long? uploadSpeed,
long? reannounce,
float? inactiveSeedingTimeLimit,
float? maxInactiveSeedingTime)
{
AddedOn = addedOn;
AmountLeft = amountLeft;
AutomaticTorrentManagement = automaticTorrentManagement;
Availability = availability;
Category = category;
Completed = completed;
CompletionOn = completionOn;
ContentPath = contentPath;
DownloadLimit = downloadLimit;
DownloadSpeed = downloadSpeed;
Downloaded = downloaded;
DownloadedSession = downloadedSession;
EstimatedTimeOfArrival = estimatedTimeOfArrival;
FirstLastPiecePriority = firstLastPiecePriority;
ForceStart = forceStart;
Hash = hash;
InfoHashV1 = infoHashV1;
InfoHashV2 = infoHashV2;
LastActivity = lastActivity;
MagnetUri = magnetUri;
MaxRatio = maxRatio;
MaxSeedingTime = maxSeedingTime;
Name = name;
NumberComplete = numberComplete;
NumberIncomplete = numberIncomplete;
NumberLeeches = numberLeeches;
NumberSeeds = numberSeeds;
Priority = priority;
Progress = progress;
Ratio = ratio;
RatioLimit = ratioLimit;
SavePath = savePath;
SeedingTime = seedingTime;
SeedingTimeLimit = seedingTimeLimit;
SeenComplete = seenComplete;
SequentialDownload = sequentialDownload;
Size = size;
State = state;
SuperSeeding = superSeeding;
Tags = tags ?? [];
TimeActive = timeActive;
TotalSize = totalSize;
Tracker = tracker;
UploadLimit = uploadLimit;
Uploaded = uploaded;
UploadedSession = uploadedSession;
UploadSpeed = uploadSpeed;
Reannounce = reannounce;
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
MaxInactiveSeedingTime = maxInactiveSeedingTime;
}
[JsonPropertyName("added_on")]
public long? AddedOn { get; }
[JsonPropertyName("amount_left")]
public long? AmountLeft { get; }
[JsonPropertyName("auto_tmm")]
public bool? AutomaticTorrentManagement { get; }
[JsonPropertyName("availability")]
public float? Availability { get; }
[JsonPropertyName("category")]
public string? Category { get; }
[JsonPropertyName("completed")]
public long? Completed { get; }
[JsonPropertyName("completion_on")]
public long? CompletionOn { get; }
[JsonPropertyName("content_path")]
public string? ContentPath { get; }
[JsonPropertyName("dl_limit")]
public long? DownloadLimit { get; }
[JsonPropertyName("dlspeed")]
public long? DownloadSpeed { get; }
[JsonPropertyName("downloaded")]
public long? Downloaded { get; }
[JsonPropertyName("downloaded_session")]
public long? DownloadedSession { get; }
[JsonPropertyName("eta")]
public long? EstimatedTimeOfArrival { get; }
[JsonPropertyName("f_l_piece_prio")]
public bool? FirstLastPiecePriority { get; }
[JsonPropertyName("force_start")]
public bool? ForceStart { get; }
[JsonPropertyName("hash")]
public string Hash { get; }
public string Hash { get; init; } = string.Empty;
[JsonPropertyName("infohash_v1")]
public string? InfoHashV1 { get; }
public string? InfoHashV1 { get; init; }
[JsonPropertyName("infohash_v2")]
public string? InfoHashV2 { get; }
[JsonPropertyName("last_activity")]
public long? LastActivity { get; }
[JsonPropertyName("magnet_uri")]
public string? MagnetUri { get; }
[JsonPropertyName("max_ratio")]
public float? MaxRatio { get; }
[JsonPropertyName("max_seeding_time")]
public int? MaxSeedingTime { get; }
public string? InfoHashV2 { get; init; }
[JsonPropertyName("name")]
public string? Name { get; }
public string? Name { get; init; }
[JsonPropertyName("num_complete")]
public int? NumberComplete { get; }
[JsonPropertyName("num_incomplete")]
public int? NumberIncomplete { get; }
[JsonPropertyName("num_leechs")]
public int? NumberLeeches { get; }
[JsonPropertyName("num_seeds")]
public int? NumberSeeds { get; }
[JsonPropertyName("priority")]
public int? Priority { get; }
[JsonPropertyName("progress")]
public float? Progress { get; }
[JsonPropertyName("ratio")]
public float? Ratio { get; }
[JsonPropertyName("ratio_limit")]
public float? RatioLimit { get; }
[JsonPropertyName("save_path")]
public string? SavePath { get; }
[JsonPropertyName("seeding_time")]
public long? SeedingTime { get; }
[JsonPropertyName("seeding_time_limit")]
public int? SeedingTimeLimit { get; }
[JsonPropertyName("seen_complete")]
public long? SeenComplete { get; }
[JsonPropertyName("seq_dl")]
public bool? SequentialDownload { get; }
[JsonPropertyName("magnet_uri")]
public string? MagnetUri { get; init; }
[JsonPropertyName("size")]
public long? Size { get; }
public long? Size { get; init; }
[JsonPropertyName("progress")]
public float? Progress { get; init; }
[JsonPropertyName("dlspeed")]
public long? DownloadSpeed { get; init; }
[JsonPropertyName("upspeed")]
public long? UploadSpeed { get; init; }
[JsonPropertyName("priority")]
public int? Priority { get; init; }
[JsonPropertyName("num_seeds")]
public int? NumberSeeds { get; init; }
[JsonPropertyName("num_complete")]
public int? NumberComplete { get; init; }
[JsonPropertyName("num_leechs")]
public int? NumberLeeches { get; init; }
[JsonPropertyName("num_incomplete")]
public int? NumberIncomplete { get; init; }
[JsonPropertyName("ratio")]
public float? Ratio { get; init; }
[JsonPropertyName("popularity")]
public float? Popularity { get; init; }
[JsonPropertyName("eta")]
public long? EstimatedTimeOfArrival { get; init; }
[JsonPropertyName("state")]
public string? State { get; }
public string? State { get; init; }
[JsonPropertyName("super_seeding")]
public bool? SuperSeeding { get; }
[JsonPropertyName("seq_dl")]
public bool? SequentialDownload { get; init; }
[JsonPropertyName("f_l_piece_prio")]
public bool? FirstLastPiecePriority { get; init; }
[JsonPropertyName("category")]
public string? Category { get; init; }
[JsonPropertyName("tags")]
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
public IReadOnlyList<string>? Tags { get; }
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
[JsonPropertyName("time_active")]
public int? TimeActive { get; }
[JsonPropertyName("super_seeding")]
public bool? SuperSeeding { get; init; }
[JsonPropertyName("total_size")]
public long? TotalSize { get; }
[JsonPropertyName("force_start")]
public bool? ForceStart { get; init; }
[JsonPropertyName("save_path")]
public string? SavePath { get; init; }
[JsonPropertyName("download_path")]
public string? DownloadPath { get; init; }
[JsonPropertyName("content_path")]
public string? ContentPath { get; init; }
[JsonPropertyName("root_path")]
public string? RootPath { get; init; }
[JsonPropertyName("added_on")]
public long? AddedOn { get; init; }
[JsonPropertyName("completion_on")]
public long? CompletionOn { get; init; }
[JsonPropertyName("tracker")]
public string? Tracker { get; }
public string? Tracker { get; init; }
[JsonPropertyName("trackers_count")]
public int? TrackersCount { get; init; }
[JsonPropertyName("dl_limit")]
public long? DownloadLimit { get; init; }
[JsonPropertyName("up_limit")]
public long? UploadLimit { get; }
public long? UploadLimit { get; init; }
[JsonPropertyName("downloaded")]
public long? Downloaded { get; init; }
[JsonPropertyName("uploaded")]
public long? Uploaded { get; }
public long? Uploaded { get; init; }
[JsonPropertyName("downloaded_session")]
public long? DownloadedSession { get; init; }
[JsonPropertyName("uploaded_session")]
public long? UploadedSession { get; }
public long? UploadedSession { get; init; }
[JsonPropertyName("upspeed")]
public long? UploadSpeed { get; }
[JsonPropertyName("amount_left")]
public long? AmountLeft { get; init; }
[JsonPropertyName("reannounce")]
public long? Reannounce { get; }
[JsonPropertyName("completed")]
public long? Completed { get; init; }
[JsonPropertyName("inactive_seeding_time_limit")]
public float? InactiveSeedingTimeLimit { get; }
[JsonPropertyName("connections_count")]
public int? ConnectionsCount { get; init; }
[JsonPropertyName("connections_limit")]
public int? ConnectionsLimit { get; init; }
[JsonPropertyName("max_ratio")]
public float? MaxRatio { get; init; }
[JsonPropertyName("max_seeding_time")]
public int? MaxSeedingTime { get; init; }
[JsonPropertyName("max_inactive_seeding_time")]
public float? MaxInactiveSeedingTime { get; }
public float? MaxInactiveSeedingTime { get; init; }
[JsonPropertyName("ratio_limit")]
public float? RatioLimit { get; init; }
[JsonPropertyName("seeding_time_limit")]
public int? SeedingTimeLimit { get; init; }
[JsonPropertyName("inactive_seeding_time_limit")]
public float? InactiveSeedingTimeLimit { get; init; }
[JsonPropertyName("share_limit_action")]
[JsonConverter(typeof(JsonStringEnumConverter))]
public ShareLimitAction? ShareLimitAction { get; init; }
[JsonPropertyName("seen_complete")]
public long? SeenComplete { get; init; }
[JsonPropertyName("last_activity")]
public long? LastActivity { get; init; }
[JsonPropertyName("total_size")]
public long? TotalSize { get; init; }
[JsonPropertyName("auto_tmm")]
public bool? AutomaticTorrentManagement { get; init; }
[JsonPropertyName("time_active")]
public int? TimeActive { get; init; }
[JsonPropertyName("seeding_time")]
public long? SeedingTime { get; init; }
[JsonPropertyName("availability")]
public float? Availability { get; init; }
[JsonPropertyName("reannounce")]
public long? Reannounce { get; init; }
[JsonPropertyName("comment")]
public string? Comment { get; init; }
[JsonPropertyName("has_metadata")]
public bool? HasMetadata { get; init; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; init; }
[JsonPropertyName("creation_date")]
public long? CreationDate { get; init; }
[JsonPropertyName("private")]
public bool? IsPrivate { get; init; }
[JsonPropertyName("total_wasted")]
public long? TotalWasted { get; init; }
[JsonPropertyName("pieces_num")]
public int? PiecesCount { get; init; }
[JsonPropertyName("piece_size")]
public long? PieceSize { get; init; }
[JsonPropertyName("pieces_have")]
public int? PiecesHave { get; init; }
[JsonPropertyName("has_tracker_warning")]
public bool? HasTrackerWarning { get; init; }
[JsonPropertyName("has_tracker_error")]
public bool? HasTrackerError { get; init; }
[JsonPropertyName("has_other_announce_error")]
public bool? HasOtherAnnounceError { get; init; }
}
}
}

View File

@@ -0,0 +1,131 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public class TorrentCreationTaskRequest
{
public string SourcePath { get; set; } = string.Empty;
public string? TorrentFilePath { get; set; }
public int? PieceSize { get; set; }
public bool? Private { get; set; }
public bool? StartSeeding { get; set; }
public string? Comment { get; set; }
public string? Source { get; set; }
public IEnumerable<string>? Trackers { get; set; }
public IEnumerable<string>? UrlSeeds { get; set; }
public string? Format { get; set; }
public bool? OptimizeAlignment { get; set; }
public int? PaddedFileSizeLimit { get; set; }
}
public record TorrentCreationTaskStatus
{
[JsonConstructor]
public TorrentCreationTaskStatus(
string taskID,
string? sourcePath,
int? pieceSize,
bool? @private,
string? timeAdded,
string? format,
bool? optimizeAlignment,
int? paddedFileSizeLimit,
string? status,
string? comment,
string? torrentFilePath,
string? source,
IReadOnlyList<string>? trackers,
IReadOnlyList<string>? urlSeeds,
string? timeStarted,
string? timeFinished,
string? errorMessage,
double? progress)
{
TaskId = taskID;
SourcePath = sourcePath;
PieceSize = pieceSize;
Private = @private;
TimeAdded = timeAdded;
Format = format;
OptimizeAlignment = optimizeAlignment;
PaddedFileSizeLimit = paddedFileSizeLimit;
Status = status;
Comment = comment;
TorrentFilePath = torrentFilePath;
Source = source;
Trackers = trackers ?? Array.Empty<string>();
UrlSeeds = urlSeeds ?? Array.Empty<string>();
TimeStarted = timeStarted;
TimeFinished = timeFinished;
ErrorMessage = errorMessage;
Progress = progress;
}
[JsonPropertyName("taskID")]
public string TaskId { get; }
[JsonPropertyName("sourcePath")]
public string? SourcePath { get; }
[JsonPropertyName("pieceSize")]
public int? PieceSize { get; }
[JsonPropertyName("private")]
public bool? Private { get; }
[JsonPropertyName("timeAdded")]
public string? TimeAdded { get; }
[JsonPropertyName("format")]
public string? Format { get; }
[JsonPropertyName("optimizeAlignment")]
public bool? OptimizeAlignment { get; }
[JsonPropertyName("paddedFileSizeLimit")]
public int? PaddedFileSizeLimit { get; }
[JsonPropertyName("status")]
public string? Status { get; }
[JsonPropertyName("comment")]
public string? Comment { get; }
[JsonPropertyName("torrentFilePath")]
public string? TorrentFilePath { get; }
[JsonPropertyName("source")]
public string? Source { get; }
[JsonPropertyName("trackers")]
public IReadOnlyList<string> Trackers { get; }
[JsonPropertyName("urlSeeds")]
public IReadOnlyList<string> UrlSeeds { get; }
[JsonPropertyName("timeStarted")]
public string? TimeStarted { get; }
[JsonPropertyName("timeFinished")]
public string? TimeFinished { get; }
[JsonPropertyName("errorMessage")]
public string? ErrorMessage { get; }
[JsonPropertyName("progress")]
public double? Progress { get; }
}
}

View File

@@ -0,0 +1,105 @@
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record TorrentMetadata
{
[JsonConstructor]
public TorrentMetadata(
string? infoHashV1,
string? infoHashV2,
string? hash,
TorrentMetadataInfo? info,
IReadOnlyList<TorrentMetadataTracker>? trackers,
IReadOnlyList<string>? webSeeds,
string? createdBy,
long? creationDate,
string? comment)
{
InfoHashV1 = infoHashV1;
InfoHashV2 = infoHashV2;
Hash = hash;
Info = info;
Trackers = trackers ?? Array.Empty<TorrentMetadataTracker>();
WebSeeds = webSeeds ?? Array.Empty<string>();
CreatedBy = createdBy;
CreationDate = creationDate;
Comment = comment;
}
[JsonPropertyName("infohash_v1")]
public string? InfoHashV1 { get; }
[JsonPropertyName("infohash_v2")]
public string? InfoHashV2 { get; }
[JsonPropertyName("hash")]
public string? Hash { get; }
[JsonPropertyName("info")]
public TorrentMetadataInfo? Info { get; }
[JsonPropertyName("trackers")]
public IReadOnlyList<TorrentMetadataTracker> Trackers { get; }
[JsonPropertyName("webseeds")]
public IReadOnlyList<string> WebSeeds { get; }
[JsonPropertyName("created_by")]
public string? CreatedBy { get; }
[JsonPropertyName("creation_date")]
public long? CreationDate { get; }
[JsonPropertyName("comment")]
public string? Comment { get; }
}
public record TorrentMetadataInfo
{
[JsonConstructor]
public TorrentMetadataInfo(
IReadOnlyList<TorrentMetadataFile>? files,
long? length,
string? name,
long? pieceLength,
int? piecesCount,
bool? @private)
{
Files = files ?? Array.Empty<TorrentMetadataFile>();
Length = length;
Name = name;
PieceLength = pieceLength;
PiecesCount = piecesCount;
Private = @private;
}
[JsonPropertyName("files")]
public IReadOnlyList<TorrentMetadataFile> Files { get; }
[JsonPropertyName("length")]
public long? Length { get; }
[JsonPropertyName("name")]
public string? Name { get; }
[JsonPropertyName("piece_length")]
public long? PieceLength { get; }
[JsonPropertyName("pieces_num")]
public int? PiecesCount { get; }
[JsonPropertyName("private")]
public bool? Private { get; }
}
public record TorrentMetadataFile(
[property: JsonPropertyName("path")] string? Path,
[property: JsonPropertyName("length")] long? Length);
public record TorrentMetadataTracker(
[property: JsonPropertyName("url")] string? Url,
[property: JsonPropertyName("tier")] int? Tier);
}

View File

@@ -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; }

View File

@@ -1,4 +1,6 @@
using System.Text.Json.Serialization;
using System;
using System.Collections.Generic;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
@@ -13,7 +15,10 @@ namespace Lantean.QBitTorrentClient.Models
int seeds,
int leeches,
int downloads,
string message)
string message,
long? nextAnnounce,
long? minAnnounce,
IReadOnlyList<TrackerEndpoint>? endpoints)
{
Url = url;
Status = status;
@@ -23,6 +28,9 @@ namespace Lantean.QBitTorrentClient.Models
Leeches = leeches;
Downloads = downloads;
Message = message;
NextAnnounce = nextAnnounce;
MinAnnounce = minAnnounce;
Endpoints = endpoints ?? Array.Empty<TrackerEndpoint>();
}
[JsonPropertyName("url")]
@@ -48,5 +56,27 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("msg")]
public string Message { get; }
[JsonPropertyName("next_announce")]
public long? NextAnnounce { get; }
[JsonPropertyName("min_announce")]
public long? MinAnnounce { get; }
[JsonPropertyName("endpoints")]
public IReadOnlyList<TrackerEndpoint> Endpoints { get; }
}
}
public record TrackerEndpoint(
[property: JsonPropertyName("name")] string? Name,
[property: JsonPropertyName("updating")] bool? Updating,
[property: JsonPropertyName("status")] TrackerStatus Status,
[property: JsonPropertyName("msg")] string? Message,
[property: JsonPropertyName("bt_version")] int? BitTorrentVersion,
[property: JsonPropertyName("num_peers")] int? Peers,
[property: JsonPropertyName("num_seeds")] int? Seeds,
[property: JsonPropertyName("num_leeches")] int? Leeches,
[property: JsonPropertyName("num_downloaded")] int? Downloads,
[property: JsonPropertyName("next_announce")] long? NextAnnounce,
[property: JsonPropertyName("min_announce")] long? MinAnnounce);
}

View File

@@ -7,5 +7,7 @@
Working = 2,
Updating = 3,
NotWorking = 4,
Error = 5,
Unreachable = 6
}
}
}

Some files were not shown because too many files have changed in this diff Show More