mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-11-04 22:13:14 +00:00
Compare commits
49 Commits
1.0.0
...
feature/ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0db0ad4374 | ||
|
|
c390d83e4d | ||
|
|
8dd29c238d | ||
|
|
fca17edfd1 | ||
|
|
d8535fa262 | ||
|
|
1c6bfed6ee | ||
|
|
281caf8026 | ||
|
|
ff905e7cac | ||
|
|
cb80dd0d6b | ||
|
|
9113fb90ee | ||
|
|
d8b4e932d1 | ||
|
|
3d0d211d10 | ||
|
|
7db4f2f78d | ||
|
|
1f606b4449 | ||
|
|
88d66b4887 | ||
|
|
2ad7be1073 | ||
|
|
300e81345c | ||
|
|
9d8d84168e | ||
|
|
bb66b97f45 | ||
|
|
4824037ba7 | ||
|
|
1f9b631a36 | ||
|
|
2c744cd972 | ||
|
|
b02bb7cfae | ||
|
|
e4dac8556e | ||
|
|
a9a8a4eba8 | ||
|
|
bb524450f0 | ||
|
|
d4ac79af00 | ||
|
|
7370d73c59 | ||
|
|
8796cc0f24 | ||
|
|
b24ae440d4 | ||
|
|
bb90ce5216 | ||
|
|
4eaa46b2b3 | ||
|
|
1cf9f97187 | ||
|
|
4f9129fd46 | ||
|
|
9a9d2c2ee2 | ||
|
|
736bc46745 | ||
|
|
23ae19c4c7 | ||
|
|
603470eb30 | ||
|
|
27c2406340 | ||
|
|
4578dcc11f | ||
|
|
3215fa3936 | ||
|
|
78e62f31d0 | ||
|
|
e23842fcb0 | ||
|
|
411c7f87cc | ||
|
|
4098f8f5a9 | ||
|
|
12f81c5978 | ||
|
|
717738d720 | ||
|
|
885c34c8cf | ||
|
|
ef3c68a6aa |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -361,3 +361,4 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/output
|
||||||
|
|||||||
@@ -4,24 +4,20 @@
|
|||||||
<TargetFramework>net9.0</TargetFramework>
|
<TargetFramework>net9.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<Nullable>enable</Nullable>
|
<Nullable>enable</Nullable>
|
||||||
|
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||||
<IsPackable>false</IsPackable>
|
<IsPackable>false</IsPackable>
|
||||||
<IsTestProject>true</IsTestProject>
|
<IsTestProject>true</IsTestProject>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="FluentAssertions" Version="7.1.0" AllowedVersions="[5.0.0,7.*.*)" />
|
<PackageReference Include="AwesomeAssertions" Version="9.2.1" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
|
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
|
||||||
<PackageReference Include="MudBlazor" Version="8.2.0" />
|
|
||||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||||
<PackageReference Include="xunit" Version="2.9.3" />
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBitTorrentClient.Models;
|
using Lantean.QBitTorrentClient.Models;
|
||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Test
|
|||||||
Test2(a => a.Name);
|
Test2(a => a.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Test2(Expression<Func<TestClass, object>> expr)
|
private void Test2(Expression<Func<TestClass, object?>> expr)
|
||||||
{
|
{
|
||||||
var body = expr.Body;
|
var body = expr.Body;
|
||||||
}
|
}
|
||||||
@@ -38,7 +38,7 @@ namespace Lantean.QBTMud.Test
|
|||||||
|
|
||||||
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
|
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
|
||||||
|
|
||||||
Expression<Func<TestClass, object>> expr2 = a => a.Name;
|
Expression<Func<TestClass, object?>> expr2 = a => a.Name;
|
||||||
|
|
||||||
var x = l.Compile();
|
var x = l.Compile();
|
||||||
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
|
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
|
||||||
@@ -58,9 +58,9 @@ namespace Lantean.QBTMud.Test
|
|||||||
|
|
||||||
public class TestClass
|
public class TestClass
|
||||||
{
|
{
|
||||||
public string Name { get; set; }
|
public string? Name { get; set; }
|
||||||
|
|
||||||
public string Description { get; set; }
|
public string? Description { get; set; }
|
||||||
|
|
||||||
public long Value { get; set; }
|
public long Value { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution
|
|||||||
readme.md = readme.md
|
readme.md = readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.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.ActiveCfg = Release|Any CPU
|
||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = 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
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> Tags { get; } = [];
|
protected HashSet<string> Tags { get; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class AddTorrentFileDialog
|
public partial class AddTorrentFileDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
|
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Url { get; set; }
|
public string? Url { get; set; }
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.Dialogs
|
namespace Lantean.QBTMud.Components.Dialogs
|
||||||
{
|
{
|
||||||
@@ -54,7 +53,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
TorrentManagementMode = preferences.AutoTmmEnabled;
|
TorrentManagementMode = preferences.AutoTmmEnabled;
|
||||||
SavePath = preferences.SavePath;
|
SavePath = preferences.SavePath;
|
||||||
StartTorrent = !preferences.StartPausedEnabled;
|
StartTorrent = !preferences.AddStoppedEnabled;
|
||||||
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
||||||
StopCondition = preferences.TorrentStopCondition;
|
StopCondition = preferences.TorrentStopCondition;
|
||||||
ContentLayout = preferences.TorrentContentLayout;
|
ContentLayout = preferences.TorrentContentLayout;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class AddTrackerDialog
|
public partial class AddTrackerDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> Trackers { get; } = [];
|
protected HashSet<string> Trackers { get; } = [];
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private string _savePath = string.Empty;
|
private string _savePath = string.Empty;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ConfirmDialog
|
public partial class ConfirmDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Content { get; set; } = default!;
|
public string Content { get; set; } = default!;
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class DeleteDialog
|
public partial class DeleteDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ExceptionDialog
|
public partial class ExceptionDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Exception? Exception { get; set; }
|
public Exception? Exception { get; set; }
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IEnumerable<string> Hashes { get; set; } = [];
|
public IEnumerable<string> Hashes { get; set; } = [];
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IEnumerable<string> Hashes { get; set; } = [];
|
public IEnumerable<string> Hashes { get; set; } = [];
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class MultipleFieldDialog
|
public partial class MultipleFieldDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Label { get; set; } = default!;
|
public string Label { get; set; } = default!;
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Hash { get; set; }
|
public string? Hash { get; set; }
|
||||||
@@ -426,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
{
|
{
|
||||||
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
|
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
|
|||||||
@@ -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" />
|
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<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="@("default")">Use global settings</MudSelectItem>
|
||||||
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
||||||
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private readonly List<string> _unsavedRuleNames = [];
|
private readonly List<string> _unsavedRuleNames = [];
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
SelectedRule.IgnoreDays = value;
|
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)
|
switch (value)
|
||||||
{
|
{
|
||||||
case "default":
|
case "default":
|
||||||
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
switch (SelectedRule.TorrentParams.Stopped)
|
switch (SelectedRule.TorrentParams.Stopped)
|
||||||
{
|
{
|
||||||
case null:
|
case null:
|
||||||
AddPaused = "default";
|
AddStopped = "default";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
AddPaused = "always";
|
AddStopped = "always";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false:
|
case false:
|
||||||
AddPaused = "never";
|
AddStopped = "never";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ShareRatioDialog
|
public partial class ShareRatioDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class SliderFieldDialog<T> where T : struct, INumber<T>
|
public partial class SliderFieldDialog<T> where T : struct, INumber<T>
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class StringFieldDialog
|
public partial class StringFieldDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class SubMenuDialog
|
public partial class SubMenuDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public UIAction? ParentAction { get; set; }
|
public UIAction? ParentAction { get; set; }
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class TorrentOptionsDialog
|
public partial class TorrentOptionsDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[EditorRequired]
|
[EditorRequired]
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar content-panel__toolbar--scroll">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
|
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
|
||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content-panel__body">
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
@ref="Table"
|
@ref="Table"
|
||||||
T="ContentItem"
|
T="ContentItem"
|
||||||
@@ -38,8 +39,10 @@
|
|||||||
SortDirectionChanged="SortDirectionChanged"
|
SortDirectionChanged="SortDirectionChanged"
|
||||||
OnTableDataContextMenu="TableDataContextMenu"
|
OnTableDataContextMenu="TableDataContextMenu"
|
||||||
OnTableDataLongPress="TableDataLongPress"
|
OnTableDataLongPress="TableDataLongPress"
|
||||||
Class="file-list"
|
Class="file-list content-panel__table"
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private RenderFragment<RowContext<ContentItem>> NameColumn
|
private RenderFragment<RowContext<ContentItem>> NameColumn
|
||||||
|
|||||||
@@ -20,6 +20,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
private readonly CancellationTokenSource _timerCancellationToken = new();
|
private readonly CancellationTokenSource _timerCancellationToken = new();
|
||||||
private bool _disposedValue;
|
private bool _disposedValue;
|
||||||
|
private static readonly ReadOnlyCollection<ContentItem> EmptyContentItems = new ReadOnlyCollection<ContentItem>(Array.Empty<ContentItem>());
|
||||||
|
private ReadOnlyCollection<ContentItem> _visibleFiles = EmptyContentItems;
|
||||||
|
private bool _filesDirty = true;
|
||||||
|
|
||||||
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
|
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
|
||||||
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
|
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
|
||||||
@@ -65,7 +68,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
private DynamicTable<ContentItem>? Table { get; set; }
|
private DynamicTable<ContentItem>? Table { get; set; }
|
||||||
|
|
||||||
private ContextMenu? ContextMenu { get; set; }
|
private MudMenu? ContextMenu { get; set; }
|
||||||
|
|
||||||
public FilesTab()
|
public FilesTab()
|
||||||
{
|
{
|
||||||
@@ -102,6 +105,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
if (_filterDefinitions is null)
|
if (_filterDefinitions is null)
|
||||||
{
|
{
|
||||||
Filters = null;
|
Filters = null;
|
||||||
|
MarkFilesDirty();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,11 +117,13 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
Filters = filters;
|
Filters = filters;
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void RemoveFilter()
|
protected void RemoveFilter()
|
||||||
{
|
{
|
||||||
Filters = null;
|
Filters = null;
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
@@ -157,6 +163,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected void SearchTextChanged(string value)
|
protected void SearchTextChanged(string value)
|
||||||
{
|
{
|
||||||
SearchText = value;
|
SearchText = value;
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
|
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
|
||||||
@@ -178,7 +185,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ContextMenu.OpenMenuAsync(eventArgs);
|
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
@@ -197,6 +206,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
||||||
{
|
{
|
||||||
|
var hasUpdates = false;
|
||||||
if (Active && Hash is not null)
|
if (Active && Hash is not null)
|
||||||
{
|
{
|
||||||
IReadOnlyList<QBitTorrentClient.Models.FileData> files;
|
IReadOnlyList<QBitTorrentClient.Models.FileData> files;
|
||||||
@@ -213,17 +223,23 @@ namespace Lantean.QBTMud.Components
|
|||||||
if (FileList is null)
|
if (FileList is null)
|
||||||
{
|
{
|
||||||
FileList = DataManager.CreateContentsList(files);
|
FileList = DataManager.CreateContentsList(files);
|
||||||
|
hasUpdates = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeContentsList(files, FileList);
|
hasUpdates = DataManager.MergeContentsList(files, FileList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasUpdates)
|
||||||
|
{
|
||||||
|
MarkFilesDirty();
|
||||||
|
PruneSelectionIfMissing();
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
protected override async Task OnParametersSetAsync()
|
||||||
{
|
{
|
||||||
@@ -246,6 +262,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
var contents = await ApiClient.GetTorrentContents(Hash);
|
var contents = await ApiClient.GetTorrentContents(Hash);
|
||||||
FileList = DataManager.CreateContentsList(contents);
|
FileList = DataManager.CreateContentsList(contents);
|
||||||
|
MarkFilesDirty();
|
||||||
|
PruneSelectionIfMissing();
|
||||||
|
|
||||||
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
|
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
|
||||||
if (expandedNodes is not null)
|
if (expandedNodes is not null)
|
||||||
@@ -256,6 +274,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
ExpandedNodes.Clear();
|
ExpandedNodes.Clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
|
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
|
||||||
@@ -320,11 +340,13 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected void SortColumnChanged(string sortColumn)
|
protected void SortColumnChanged(string sortColumn)
|
||||||
{
|
{
|
||||||
_sortColumn = sortColumn;
|
_sortColumn = sortColumn;
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SortDirectionChanged(SortDirection sortDirection)
|
protected void SortDirectionChanged(SortDirection sortDirection)
|
||||||
{
|
{
|
||||||
_sortDirection = sortDirection;
|
_sortDirection = sortDirection;
|
||||||
|
MarkFilesDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SelectedItemChanged(ContentItem item)
|
protected void SelectedItemChanged(ContentItem item)
|
||||||
@@ -343,6 +365,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
ExpandedNodes.Add(contentItem.Name);
|
ExpandedNodes.Add(contentItem.Name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkFilesDirty();
|
||||||
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
|
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,44 +391,6 @@ namespace Lantean.QBTMud.Components
|
|||||||
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
|
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<ContentItem> GetChildren(ContentItem folder, int level)
|
|
||||||
{
|
|
||||||
level++;
|
|
||||||
var descendantsKey = folder.GetDescendantsKey(level);
|
|
||||||
|
|
||||||
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey) && f.Level == level).OrderByDirection(_sortDirection, GetSortSelector()))
|
|
||||||
{
|
|
||||||
if (item.IsFolder)
|
|
||||||
{
|
|
||||||
var descendants = GetChildren(item, level);
|
|
||||||
// if the filter returns some results then show folder item
|
|
||||||
if (descendants.Any())
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
|
|
||||||
// if the folder is not expanded - don't return children
|
|
||||||
if (!ExpandedNodes.Contains(item.Name))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// then show children
|
|
||||||
foreach (var descendant in descendants)
|
|
||||||
{
|
|
||||||
yield return descendant;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (FilterContentItem(item))
|
|
||||||
{
|
|
||||||
yield return item;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool FilterContentItem(ContentItem item)
|
private bool FilterContentItem(ContentItem item)
|
||||||
{
|
{
|
||||||
if (Filters is not null)
|
if (Filters is not null)
|
||||||
@@ -429,38 +414,130 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
|
|
||||||
private ReadOnlyCollection<ContentItem> GetFiles()
|
private ReadOnlyCollection<ContentItem> GetFiles()
|
||||||
|
{
|
||||||
|
if (!_filesDirty)
|
||||||
|
{
|
||||||
|
return _visibleFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
_visibleFiles = BuildVisibleFiles();
|
||||||
|
_filesDirty = false;
|
||||||
|
|
||||||
|
return _visibleFiles;
|
||||||
|
}
|
||||||
|
|
||||||
|
private ReadOnlyCollection<ContentItem> BuildVisibleFiles()
|
||||||
{
|
{
|
||||||
if (FileList is null || FileList.Values.Count == 0)
|
if (FileList is null || FileList.Values.Count == 0)
|
||||||
{
|
{
|
||||||
return new ReadOnlyCollection<ContentItem>([]);
|
return EmptyContentItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
var maxLevel = FileList.Values.Max(f => f.Level);
|
var lookup = BuildChildrenLookup();
|
||||||
// this is a flat file structure
|
if (!lookup.TryGetValue(string.Empty, out var roots))
|
||||||
if (maxLevel == 0)
|
|
||||||
{
|
{
|
||||||
return FileList.Values.Where(FilterContentItem).OrderByDirection(_sortDirection, GetSortSelector()).ToList().AsReadOnly();
|
return EmptyContentItems;
|
||||||
}
|
}
|
||||||
|
|
||||||
var list = new List<ContentItem>();
|
var sortSelector = GetSortSelector();
|
||||||
|
var orderedRoots = roots.OrderByDirection(_sortDirection, sortSelector).ToList();
|
||||||
|
var result = new List<ContentItem>(FileList.Values.Count);
|
||||||
|
|
||||||
var rootItems = FileList.Values.Where(c => c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
|
foreach (var item in orderedRoots)
|
||||||
foreach (var item in rootItems)
|
|
||||||
{
|
{
|
||||||
list.Add(item);
|
if (item.IsFolder)
|
||||||
|
{
|
||||||
|
result.Add(item);
|
||||||
|
|
||||||
if (item.IsFolder && ExpandedNodes.Contains(item.Name))
|
if (!ExpandedNodes.Contains(item.Name))
|
||||||
{
|
{
|
||||||
var level = 0;
|
continue;
|
||||||
var descendants = GetChildren(item, level);
|
}
|
||||||
foreach (var descendant in descendants)
|
|
||||||
|
var descendants = GetVisibleDescendants(item, lookup, sortSelector);
|
||||||
|
result.AddRange(descendants);
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
list.Add(descendant);
|
if (FilterContentItem(item))
|
||||||
|
{
|
||||||
|
result.Add(item);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return list.AsReadOnly();
|
return new ReadOnlyCollection<ContentItem>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, List<ContentItem>> BuildChildrenLookup()
|
||||||
|
{
|
||||||
|
var lookup = new Dictionary<string, List<ContentItem>>(FileList!.Count);
|
||||||
|
|
||||||
|
foreach (var item in FileList!.Values)
|
||||||
|
{
|
||||||
|
var parentPath = item.Level == 0 ? string.Empty : item.Name.GetDirectoryPath();
|
||||||
|
if (!lookup.TryGetValue(parentPath, out var children))
|
||||||
|
{
|
||||||
|
children = [];
|
||||||
|
lookup[parentPath] = children;
|
||||||
|
}
|
||||||
|
|
||||||
|
children.Add(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lookup;
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<ContentItem> GetVisibleDescendants(ContentItem folder, Dictionary<string, List<ContentItem>> lookup, Func<ContentItem, object?> sortSelector)
|
||||||
|
{
|
||||||
|
if (!lookup.TryGetValue(folder.Name, out var children))
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedChildren = children.OrderByDirection(_sortDirection, sortSelector).ToList();
|
||||||
|
var visible = new List<ContentItem>();
|
||||||
|
|
||||||
|
foreach (var child in orderedChildren)
|
||||||
|
{
|
||||||
|
if (child.IsFolder)
|
||||||
|
{
|
||||||
|
var descendants = GetVisibleDescendants(child, lookup, sortSelector);
|
||||||
|
if (descendants.Count != 0)
|
||||||
|
{
|
||||||
|
visible.Add(child);
|
||||||
|
|
||||||
|
if (ExpandedNodes.Contains(child.Name))
|
||||||
|
{
|
||||||
|
visible.AddRange(descendants);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (FilterContentItem(child))
|
||||||
|
{
|
||||||
|
visible.Add(child);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return visible;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkFilesDirty()
|
||||||
|
{
|
||||||
|
_filesDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PruneSelectionIfMissing()
|
||||||
|
{
|
||||||
|
if (SelectedItem is not null && (FileList is null || !FileList.ContainsKey(SelectedItem.Name)))
|
||||||
|
{
|
||||||
|
SelectedItem = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ContextMenuItem is not null && (FileList is null || !FileList.ContainsKey(ContextMenuItem.Name)))
|
||||||
|
{
|
||||||
|
ContextMenuItem = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task DoNotDownloadLessThan100PercentAvailability()
|
protected async Task DoNotDownloadLessThan100PercentAvailability()
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
|
<MudMenu @ref="StatusContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
@TorrentControls(_statusType)
|
@TorrentControls(_statusType)
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<ContextMenu @ref="CategoryContextMenu" Dense="true" AdjustmentY="-60">
|
<MudMenu @ref="CategoryContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
|
||||||
@if (IsCategoryTarget)
|
@if (IsCategoryTarget)
|
||||||
{
|
{
|
||||||
@@ -12,9 +12,9 @@
|
|||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
@TorrentControls(_categoryType)
|
@TorrentControls(_categoryType)
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<ContextMenu @ref="TagContextMenu" Dense="true" AdjustmentY="-60">
|
<MudMenu @ref="TagContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
|
||||||
@if (IsTagTarget)
|
@if (IsTagTarget)
|
||||||
{
|
{
|
||||||
@@ -23,13 +23,13 @@
|
|||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
@TorrentControls(_tagType)
|
@TorrentControls(_tagType)
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<ContextMenu @ref="TrackerContextMenu" Dense="true" AdjustmentY="-60">
|
<MudMenu @ref="TrackerContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
@TorrentControls(_trackerType)
|
@TorrentControls(_trackerType)
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<MudNavMenu Dense="true">
|
<MudNavMenu Dense="true">
|
||||||
<MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
|
<MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
|
||||||
@@ -65,8 +65,8 @@
|
|||||||
{
|
{
|
||||||
return __builder =>
|
return __builder =>
|
||||||
{
|
{
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start torrents</MudMenuItem>
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause 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>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Components.UI;
|
|
||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components
|
namespace Lantean.QBTMud.Components
|
||||||
{
|
{
|
||||||
@@ -69,13 +69,13 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected Dictionary<string, int> Statuses => GetStatuses();
|
protected Dictionary<string, int> Statuses => GetStatuses();
|
||||||
|
|
||||||
protected ContextMenu? StatusContextMenu { get; set; }
|
protected MudMenu? StatusContextMenu { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? CategoryContextMenu { get; set; }
|
protected MudMenu? CategoryContextMenu { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? TagContextMenu { get; set; }
|
protected MudMenu? TagContextMenu { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? TrackerContextMenu { get; set; }
|
protected MudMenu? TrackerContextMenu { get; set; }
|
||||||
|
|
||||||
protected string? ContextMenuStatus { get; set; }
|
protected string? ContextMenuStatus { get; set; }
|
||||||
|
|
||||||
@@ -154,7 +154,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
ContextMenuStatus = value;
|
ContextMenuStatus = value;
|
||||||
|
|
||||||
return StatusContextMenu.OpenMenuAsync(args);
|
var normalizedArgs = args.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
return StatusContextMenu.OpenMenuAsync(normalizedArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task CategoryValueChanged(string value)
|
protected async Task CategoryValueChanged(string value)
|
||||||
@@ -192,7 +194,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
|
IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
|
||||||
ContextMenuCategory = value;
|
ContextMenuCategory = value;
|
||||||
|
|
||||||
return CategoryContextMenu.OpenMenuAsync(args);
|
var normalizedArgs = args.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
return CategoryContextMenu.OpenMenuAsync(normalizedArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task TagValueChanged(string value)
|
protected async Task TagValueChanged(string value)
|
||||||
@@ -230,7 +234,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
|
IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
|
||||||
ContextMenuTag = value;
|
ContextMenuTag = value;
|
||||||
|
|
||||||
return TagContextMenu.OpenMenuAsync(args);
|
var normalizedArgs = args.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
return TagContextMenu.OpenMenuAsync(normalizedArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task TrackerValueChanged(string value)
|
protected async Task TrackerValueChanged(string value)
|
||||||
@@ -267,7 +273,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
ContextMenuTracker = value;
|
ContextMenuTracker = value;
|
||||||
|
|
||||||
return TrackerContextMenu.OpenMenuAsync(args);
|
var normalizedArgs = args.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
return TrackerContextMenu.OpenMenuAsync(normalizedArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task AddCategory()
|
protected async Task AddCategory()
|
||||||
@@ -345,18 +353,18 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task ResumeTorrents(string type)
|
protected async Task StartTorrents(string type)
|
||||||
{
|
{
|
||||||
var torrents = GetAffectedTorrentHashes(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);
|
var torrents = GetAffectedTorrentHashes(type);
|
||||||
|
|
||||||
await ApiClient.PauseTorrents(torrents);
|
await ApiClient.StopTorrents(hashes: torrents.ToArray());
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task RemoveTorrents(string type)
|
protected async Task RemoveTorrents(string type)
|
||||||
|
|||||||
@@ -92,7 +92,9 @@
|
|||||||
<FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
|
<FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="9">
|
<MudItem xs="9">
|
||||||
<MudNumericField T="int" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged" Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined" Validation="MaxRatioValidation" />
|
<MudNumericField T="float" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged"
|
||||||
|
Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined"
|
||||||
|
Validation="MaxRatioValidation" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="3">
|
<MudItem xs="3">
|
||||||
<FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
|
<FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
protected int SlowTorrentUlRateThreshold { get; private set; }
|
protected int SlowTorrentUlRateThreshold { get; private set; }
|
||||||
protected int SlowTorrentInactiveTimer { get; private set; }
|
protected int SlowTorrentInactiveTimer { get; private set; }
|
||||||
protected bool MaxRatioEnabled { get; private set; }
|
protected bool MaxRatioEnabled { get; private set; }
|
||||||
protected int MaxRatio { get; private set; }
|
protected float MaxRatio { get; private set; }
|
||||||
protected bool MaxSeedingTimeEnabled { get; private set; }
|
protected bool MaxSeedingTimeEnabled { get; private set; }
|
||||||
protected int MaxSeedingTime { get; private set; }
|
protected int MaxSeedingTime { get; private set; }
|
||||||
protected int MaxRatioAct { get; private set; }
|
protected int MaxRatioAct { get; private set; }
|
||||||
@@ -275,7 +275,7 @@
|
|||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task MaxRatioChanged(int value)
|
protected async Task MaxRatioChanged(float value)
|
||||||
{
|
{
|
||||||
MaxRatio = value;
|
MaxRatio = value;
|
||||||
UpdatePreferences.MaxRatio = value;
|
UpdatePreferences.MaxRatio = value;
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
|
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<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>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
|
||||||
@@ -62,7 +62,7 @@
|
|||||||
<MudCardContent Class="pt-0">
|
<MudCardContent Class="pt-0">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" Variant="Variant.Outlined">
|
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoTmmEnabledChanged" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
<MudSelectItem Value="false">Manual</MudSelectItem>
|
||||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
||||||
</MudSelect>
|
</MudSelect>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
{
|
{
|
||||||
protected string? TorrentContentLayout { get; set; }
|
protected string? TorrentContentLayout { get; set; }
|
||||||
protected bool AddToTopOfQueue { get; set; }
|
protected bool AddToTopOfQueue { get; set; }
|
||||||
protected bool StartPausedEnabled { get; set; }
|
protected bool AddStoppedEnabled { get; set; }
|
||||||
protected string? TorrentStopCondition { get; set; }
|
protected string? TorrentStopCondition { get; set; }
|
||||||
protected bool AutoDeleteMode { get; set; }
|
protected bool AutoDeleteMode { get; set; }
|
||||||
protected bool PreallocateAll { get; set; }
|
protected bool PreallocateAll { get; set; }
|
||||||
@@ -51,7 +51,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
// when adding a torrent
|
// when adding a torrent
|
||||||
TorrentContentLayout = Preferences.TorrentContentLayout;
|
TorrentContentLayout = Preferences.TorrentContentLayout;
|
||||||
AddToTopOfQueue = Preferences.AddToTopOfQueue;
|
AddToTopOfQueue = Preferences.AddToTopOfQueue;
|
||||||
StartPausedEnabled = Preferences.StartPausedEnabled;
|
AddStoppedEnabled = Preferences.AddStoppedEnabled;
|
||||||
TorrentStopCondition = Preferences.TorrentStopCondition;
|
TorrentStopCondition = Preferences.TorrentStopCondition;
|
||||||
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
|
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
|
||||||
PreallocateAll = Preferences.PreallocateAll;
|
PreallocateAll = Preferences.PreallocateAll;
|
||||||
@@ -116,10 +116,10 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task StartPausedEnabledChanged(bool value)
|
protected async Task AddStoppedEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
StartPausedEnabled = value;
|
AddStoppedEnabled = value;
|
||||||
UpdatePreferences.StartPausedEnabled = value;
|
UpdatePreferences.AddStoppedEnabled = value;
|
||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,22 @@
|
|||||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
|
||||||
@if (ContextMenuItem is not null)
|
@if (ContextMenuItem is not null)
|
||||||
{
|
{
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
|
||||||
}
|
}
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
|
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
|
<MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
|
||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<DynamicTable T="Peer"
|
<DynamicTable T="Peer"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
Items="Peers"
|
Items="Peers"
|
||||||
@@ -21,4 +25,6 @@
|
|||||||
OnTableDataLongPress="TableDataLongPress"
|
OnTableDataLongPress="TableDataLongPress"
|
||||||
OnTableDataContextMenu="TableDataContextMenu"
|
OnTableDataContextMenu="TableDataContextMenu"
|
||||||
SelectedItemChanged="SelectedItemChanged"
|
SelectedItemChanged="SelectedItemChanged"
|
||||||
Class="details-list" />
|
Class="details-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected Peer? SelectedItem { get; set; }
|
protected Peer? SelectedItem { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? ContextMenu { get; set; }
|
protected MudMenu? ContextMenu { get; set; }
|
||||||
|
|
||||||
protected DynamicTable<Peer>? Table { get; set; }
|
protected DynamicTable<Peer>? Table { get; set; }
|
||||||
|
|
||||||
@@ -153,7 +153,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ContextMenu.ToggleMenuAsync(eventArgs);
|
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SelectedItemChanged(Peer peer)
|
protected void SelectedItemChanged(Peer peer)
|
||||||
|
|||||||
@@ -7,15 +7,13 @@ using Lantean.QBTMud.Services;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using Microsoft.JSInterop;
|
using Microsoft.JSInterop;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components
|
namespace Lantean.QBTMud.Components
|
||||||
{
|
{
|
||||||
public partial class TorrentActions : IAsyncDisposable
|
public partial class TorrentActions : IAsyncDisposable
|
||||||
{
|
{
|
||||||
private const int _defaultVersion = 5;
|
|
||||||
|
|
||||||
private bool _disposedValue;
|
private bool _disposedValue;
|
||||||
private int? _version;
|
|
||||||
|
|
||||||
private List<UIAction>? _actions;
|
private List<UIAction>? _actions;
|
||||||
|
|
||||||
@@ -40,9 +38,6 @@ namespace Lantean.QBTMud.Components
|
|||||||
[Inject]
|
[Inject]
|
||||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter(Name = "Version")]
|
|
||||||
public string? Version { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[EditorRequired]
|
[EditorRequired]
|
||||||
public IEnumerable<string> Hashes { get; set; } = default!;
|
public IEnumerable<string> Hashes { get; set; } = default!;
|
||||||
@@ -74,37 +69,12 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected bool OverlayVisible { get; set; }
|
protected bool OverlayVisible { get; set; }
|
||||||
|
|
||||||
protected int MajorVersion
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
if (_version is not null)
|
|
||||||
{
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(Version))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!System.Version.TryParse(Version.Replace("v", ""), out var version))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
_version = version.Major;
|
|
||||||
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override void OnInitialized()
|
protected override void OnInitialized()
|
||||||
{
|
{
|
||||||
_actions =
|
_actions =
|
||||||
[
|
[
|
||||||
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
|
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
|
||||||
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
|
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
|
||||||
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
|
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
|
||||||
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
|
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
|
||||||
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
|
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
|
||||||
@@ -172,33 +142,17 @@ namespace Lantean.QBTMud.Components
|
|||||||
OverlayVisible = value;
|
OverlayVisible = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task Pause()
|
protected async Task Stop()
|
||||||
{
|
{
|
||||||
if (MajorVersion < 5)
|
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
|
||||||
{
|
|
||||||
await ApiClient.PauseTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent paused.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ApiClient.StopTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent stopped.");
|
Snackbar.Add("Torrent stopped.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task Resume()
|
protected async Task Start()
|
||||||
{
|
{
|
||||||
if (MajorVersion < 5)
|
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
|
||||||
{
|
|
||||||
await ApiClient.ResumeTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent resumed.");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await ApiClient.StartTorrents(Hashes);
|
|
||||||
Snackbar.Add("Torrent started.");
|
Snackbar.Add("Torrent started.");
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ForceStart()
|
protected async Task ForceStart()
|
||||||
{
|
{
|
||||||
@@ -411,8 +365,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
var allAreFirstLastPiecePrio = true;
|
var allAreFirstLastPiecePrio = true;
|
||||||
var thereAreFirstLastPiecePrio = false;
|
var thereAreFirstLastPiecePrio = false;
|
||||||
var allAreDownloaded = true;
|
var allAreDownloaded = true;
|
||||||
var allArePaused = true;
|
var allAreStopped = true;
|
||||||
var thereArePaused = false;
|
var thereAreStopped = false;
|
||||||
var allAreForceStart = true;
|
var allAreForceStart = true;
|
||||||
var thereAreForceStart = false;
|
var thereAreForceStart = false;
|
||||||
var allAreSuperSeeding = true;
|
var allAreSuperSeeding = true;
|
||||||
@@ -441,7 +395,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
thereAreFirstLastPiecePrio = true;
|
thereAreFirstLastPiecePrio = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (torrent.Progress > 0.999999) // not downloaded
|
if (torrent.Progress < 0.999999) // not downloaded
|
||||||
{
|
{
|
||||||
allAreDownloaded = false;
|
allAreDownloaded = false;
|
||||||
}
|
}
|
||||||
@@ -450,27 +404,13 @@ namespace Lantean.QBTMud.Components
|
|||||||
allAreSuperSeeding = false;
|
allAreSuperSeeding = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MajorVersion < 5)
|
|
||||||
{
|
|
||||||
if (torrent.State != "pausedUP" && torrent.State != "pausedDL")
|
|
||||||
{
|
|
||||||
allArePaused = false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
thereArePaused = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
|
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
|
||||||
{
|
{
|
||||||
allArePaused = false;
|
allAreStopped = false;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
thereArePaused = true;
|
thereAreStopped = true;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!torrent.ForceStart)
|
if (!torrent.ForceStart)
|
||||||
@@ -558,7 +498,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
actionStates["superSeeding"] = ActionState.Hidden;
|
actionStates["superSeeding"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allArePaused)
|
if (allAreStopped)
|
||||||
{
|
{
|
||||||
actionStates["pause"] = ActionState.Hidden;
|
actionStates["pause"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
@@ -566,13 +506,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
actionStates["forceStart"] = ActionState.Hidden;
|
actionStates["forceStart"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
else if (!thereArePaused && !thereAreForceStart)
|
else if (!thereAreStopped && !thereAreForceStart)
|
||||||
{
|
{
|
||||||
actionStates["start"] = ActionState.Hidden;
|
actionStates["start"] = ActionState.Hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (MajorVersion >= 5)
|
|
||||||
{
|
|
||||||
if (actionStates.TryGetValue("start", out ActionState? startActionState))
|
if (actionStates.TryGetValue("start", out ActionState? startActionState))
|
||||||
{
|
{
|
||||||
startActionState.TextOverride = "Start";
|
startActionState.TextOverride = "Start";
|
||||||
@@ -590,7 +528,6 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
|
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!allAreAutoTmm && thereAreAutoTmm)
|
if (!allAreAutoTmm && thereAreAutoTmm)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
|
||||||
@if (ContextMenuItem is not null)
|
@if (ContextMenuItem is not null)
|
||||||
{
|
{
|
||||||
@@ -6,8 +6,10 @@
|
|||||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
|
||||||
}
|
}
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
|
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
|
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
|
||||||
@@ -16,7 +18,9 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<DynamicTable @ref="Table"
|
<DynamicTable @ref="Table"
|
||||||
T="Lantean.QBitTorrentClient.Models.TorrentTracker"
|
T="Lantean.QBitTorrentClient.Models.TorrentTracker"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
@@ -29,4 +33,6 @@
|
|||||||
OnTableDataLongPress="TableDataLongPress"
|
OnTableDataLongPress="TableDataLongPress"
|
||||||
OnTableDataContextMenu="TableDataContextMenu"
|
OnTableDataContextMenu="TableDataContextMenu"
|
||||||
SelectedItemChanged="SelectedItemChanged"
|
SelectedItemChanged="SelectedItemChanged"
|
||||||
Class="file-list" />
|
Class="file-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected TorrentTracker? SelectedItem { get; set; }
|
protected TorrentTracker? SelectedItem { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? ContextMenu { get; set; }
|
protected MudMenu? ContextMenu { get; set; }
|
||||||
|
|
||||||
protected DynamicTable<TorrentTracker>? Table { get; set; }
|
protected DynamicTable<TorrentTracker>? Table { get; set; }
|
||||||
|
|
||||||
@@ -148,7 +148,9 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ContextMenu.ToggleMenuAsync(eventArgs);
|
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SelectedItemChanged(TorrentTracker torrentTracker)
|
protected void SelectedItemChanged(TorrentTracker torrentTracker)
|
||||||
@@ -169,7 +171,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApiClient.AddTrackersToTorrent(Hash, trackers);
|
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task EditTrackerToolbar()
|
protected Task EditTrackerToolbar()
|
||||||
@@ -209,7 +211,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
|
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task CopyTrackerUrlToolbar()
|
protected Task CopyTrackerUrlToolbar()
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
@inherits MudComponentBase
|
|
||||||
|
|
||||||
<MudMenu @ref="FakeMenu" Style="display: none" OpenChanged="FakeOpenChanged"></MudMenu>
|
|
||||||
|
|
||||||
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
|
|
||||||
<MudPopover tracker="@Id"
|
|
||||||
Open="@_open"
|
|
||||||
Class="unselectable"
|
|
||||||
MaxHeight="@MaxHeight"
|
|
||||||
AnchorOrigin="@AnchorOrigin"
|
|
||||||
TransformOrigin="@TransformOrigin"
|
|
||||||
RelativeWidth="@RelativeWidth"
|
|
||||||
OverflowBehavior="OverflowBehavior.FlipAlways"
|
|
||||||
Style="@_popoverStyle"
|
|
||||||
@ontouchend:preventDefault>
|
|
||||||
<CascadingValue Value="@(FakeMenu)">
|
|
||||||
@if (_showChildren)
|
|
||||||
{
|
|
||||||
<MudList T="object" Class="unselectable" Dense="@Dense">
|
|
||||||
@ChildContent
|
|
||||||
</MudList>
|
|
||||||
}
|
|
||||||
</CascadingValue>
|
|
||||||
</MudPopover>
|
|
||||||
|
|
||||||
<MudOverlay Visible="@(_open)" LockScroll="@LockScroll" AutoClose="true" OnClosed="@CloseMenuAsync" />
|
|
||||||
@@ -1,290 +0,0 @@
|
|||||||
using Lantean.QBTMud.Interop;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
|
||||||
using Microsoft.JSInterop;
|
|
||||||
using MudBlazor;
|
|
||||||
using MudBlazor.Utilities;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.UI
|
|
||||||
{
|
|
||||||
public partial class ContextMenu : MudComponentBase
|
|
||||||
{
|
|
||||||
private bool _open;
|
|
||||||
private bool _showChildren;
|
|
||||||
private string? _popoverStyle;
|
|
||||||
private string? _id;
|
|
||||||
|
|
||||||
private double _x;
|
|
||||||
private double _y;
|
|
||||||
private bool _isResized = false;
|
|
||||||
|
|
||||||
private const double _diff = 64;
|
|
||||||
|
|
||||||
private string Id
|
|
||||||
{
|
|
||||||
get
|
|
||||||
{
|
|
||||||
_id ??= Guid.NewGuid().ToString();
|
|
||||||
|
|
||||||
return _id;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
public IJSRuntime JSRuntime { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
public IPopoverService PopoverService { get; set; } = default!;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, compact vertical padding will be applied to all menu items.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public bool Dense { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set to true if you want to prevent page from scrolling when the menu is open
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public bool LockScroll { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, the list menu will be same width as the parent.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public DropdownWidth RelativeWidth { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the max height the menu can have when open.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public int? MaxHeight { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Set the anchor origin point to determine where the popover will open from.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public Origin AnchorOrigin { get; set; } = Origin.TopLeft;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Sets the transform origin point for the popover.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
|
||||||
public Origin TransformOrigin { get; set; } = Origin.TopLeft;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// If true, menu will be disabled.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.Behavior)]
|
|
||||||
public bool Disabled { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.Appearance)]
|
|
||||||
public bool Ripple { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Determines whether the component has a drop-shadow. Default is true
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.Appearance)]
|
|
||||||
public bool DropShadow { get; set; } = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Add menu items here
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupBehavior)]
|
|
||||||
public RenderFragment? ChildContent { get; set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Fired when the menu <see cref="Open"/> property changes.
|
|
||||||
/// </summary>
|
|
||||||
[Parameter]
|
|
||||||
[Category(CategoryTypes.Menu.PopupBehavior)]
|
|
||||||
public EventCallback<bool> OpenChanged { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public int AdjustmentX { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public int AdjustmentY { get; set; }
|
|
||||||
|
|
||||||
protected MudMenu? FakeMenu { get; set; }
|
|
||||||
|
|
||||||
protected void FakeOpenChanged(bool value)
|
|
||||||
{
|
|
||||||
if (!value)
|
|
||||||
{
|
|
||||||
_open = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the menu.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">
|
|
||||||
/// The arguments of the calling mouse/pointer event.
|
|
||||||
/// </param>
|
|
||||||
public async Task OpenMenuAsync(EventArgs args)
|
|
||||||
{
|
|
||||||
if (Disabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// long press on iOS triggers selection, so clear it
|
|
||||||
await JSRuntime.ClearSelection();
|
|
||||||
|
|
||||||
if (args is not LongPressEventArgs)
|
|
||||||
{
|
|
||||||
_showChildren = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
_open = true;
|
|
||||||
_isResized = false;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
var (x, y) = GetPositionFromArgs(args);
|
|
||||||
_x = x;
|
|
||||||
_y = y;
|
|
||||||
|
|
||||||
SetPopoverStyle(x, y);
|
|
||||||
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
await OpenChanged.InvokeAsync(_open);
|
|
||||||
|
|
||||||
// long press on iOS triggers selection, so clear it
|
|
||||||
await JSRuntime.ClearSelection();
|
|
||||||
|
|
||||||
if (args is LongPressEventArgs)
|
|
||||||
{
|
|
||||||
await Task.Delay(1000);
|
|
||||||
_showChildren = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Closes the menu.
|
|
||||||
/// </summary>
|
|
||||||
public Task CloseMenuAsync()
|
|
||||||
{
|
|
||||||
_open = false;
|
|
||||||
_popoverStyle = null;
|
|
||||||
StateHasChanged();
|
|
||||||
|
|
||||||
return OpenChanged.InvokeAsync(_open);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void SetPopoverStyle(double x, double y)
|
|
||||||
{
|
|
||||||
_popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};";
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Toggle the visibility of the menu.
|
|
||||||
/// </summary>
|
|
||||||
public async Task ToggleMenuAsync(EventArgs args)
|
|
||||||
{
|
|
||||||
if (Disabled)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_open)
|
|
||||||
{
|
|
||||||
await CloseMenuAsync();
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await OpenMenuAsync(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!_isResized)
|
|
||||||
{
|
|
||||||
//await DeterminePosition();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
//private async Task DeterminePosition()
|
|
||||||
//{
|
|
||||||
// var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
|
|
||||||
// double? contextMenuHeight = null;
|
|
||||||
// double? contextMenuWidth = null;
|
|
||||||
|
|
||||||
// var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
|
|
||||||
|
|
||||||
// var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
|
|
||||||
// if (popoverSize.Height > 0)
|
|
||||||
// {
|
|
||||||
// contextMenuHeight = popoverSize.Height;
|
|
||||||
// contextMenuWidth = popoverSize.Width;
|
|
||||||
// }
|
|
||||||
// else
|
|
||||||
// {
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// // the bottom position of the popover will be rendered off screen
|
|
||||||
// if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
|
|
||||||
// {
|
|
||||||
// // adjust the top of the context menu
|
|
||||||
// var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
|
|
||||||
// _y -= overshoot;
|
|
||||||
|
|
||||||
// if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
|
|
||||||
// {
|
|
||||||
// MaxHeight = (int)(mainContentSize.Height - _y + _diff);
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// if (_x + contextMenuWidth.Value > mainContentSize.Width)
|
|
||||||
// {
|
|
||||||
// var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
|
|
||||||
// _x -= overshoot;
|
|
||||||
// }
|
|
||||||
|
|
||||||
// SetPopoverStyle(_x, _y);
|
|
||||||
// _isResized = true;
|
|
||||||
// await InvokeAsync(StateHasChanged);
|
|
||||||
//}
|
|
||||||
|
|
||||||
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
|
|
||||||
{
|
|
||||||
double x, y;
|
|
||||||
if (eventArgs is MouseEventArgs mouseEventArgs)
|
|
||||||
{
|
|
||||||
x = mouseEventArgs.ClientX;
|
|
||||||
y = mouseEventArgs.ClientY;
|
|
||||||
}
|
|
||||||
else if (eventArgs is LongPressEventArgs longPressEventArgs)
|
|
||||||
{
|
|
||||||
x = longPressEventArgs.ClientX;
|
|
||||||
y = longPressEventArgs.ClientY;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new NotSupportedException("Invalid eventArgs type.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (x + AdjustmentX, y + AdjustmentY);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<div class="@Classname">
|
<div class="@Classname">
|
||||||
<div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
<div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
||||||
@if (!string.IsNullOrEmpty(Icon))
|
@if (!string.IsNullOrEmpty(Icon))
|
||||||
{
|
{
|
||||||
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
|
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
new CssBuilder("mud-nav-link")
|
new CssBuilder("mud-nav-link")
|
||||||
.AddClass($"mud-nav-link-disabled", Disabled)
|
.AddClass($"mud-nav-link-disabled", Disabled)
|
||||||
.AddClass("active", Active)
|
.AddClass("active", Active)
|
||||||
|
.AddClass("unselectable", OnLongPress.HasDelegate || OnContextMenu.HasDelegate)
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
protected string IconClassname =>
|
protected string IconClassname =>
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
|
|
||||||
protected HashSet<string> SelectedColumns { get; set; } = [];
|
protected HashSet<string> SelectedColumns { get; set; } = [];
|
||||||
|
|
||||||
|
private static readonly IReadOnlyList<ColumnDefinition<T>> EmptyColumns = Array.Empty<ColumnDefinition<T>>();
|
||||||
|
|
||||||
private Dictionary<string, int?> _columnWidths = [];
|
private Dictionary<string, int?> _columnWidths = [];
|
||||||
|
|
||||||
private Dictionary<string, int> _columnOrder = [];
|
private Dictionary<string, int> _columnOrder = [];
|
||||||
@@ -89,8 +91,16 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
|
|
||||||
private SortDirection _sortDirection;
|
private SortDirection _sortDirection;
|
||||||
|
|
||||||
|
private DateTimeOffset? _suppressRowClickUntil;
|
||||||
|
|
||||||
private readonly Dictionary<string, TdExtended> _tds = [];
|
private readonly Dictionary<string, TdExtended> _tds = [];
|
||||||
|
|
||||||
|
private IReadOnlyList<ColumnDefinition<T>> _visibleColumns = EmptyColumns;
|
||||||
|
|
||||||
|
private bool _columnsDirty = true;
|
||||||
|
|
||||||
|
private IEnumerable<ColumnDefinition<T>>? _lastColumnDefinitions;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
HashSet<string> selectedColumns;
|
HashSet<string> selectedColumns;
|
||||||
@@ -109,6 +119,13 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
SelectedColumns = selectedColumns;
|
SelectedColumns = selectedColumns;
|
||||||
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SelectedColumns = selectedColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lastColumnDefinitions = ColumnDefinitions;
|
||||||
|
MarkColumnsDirty();
|
||||||
|
|
||||||
string? sortColumn;
|
string? sortColumn;
|
||||||
SortDirection sortDirection;
|
SortDirection sortDirection;
|
||||||
@@ -137,11 +154,24 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
await SortDirectionChanged.InvokeAsync(_sortDirection);
|
await SortDirectionChanged.InvokeAsync(_sortDirection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MarkColumnsDirty();
|
||||||
|
|
||||||
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
|
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
|
||||||
if (storedColumnsWidths is not null)
|
if (storedColumnsWidths is not null)
|
||||||
{
|
{
|
||||||
_columnWidths = storedColumnsWidths;
|
_columnWidths = storedColumnsWidths;
|
||||||
}
|
}
|
||||||
|
MarkColumnsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnParametersSet()
|
||||||
|
{
|
||||||
|
base.OnParametersSet();
|
||||||
|
if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
|
||||||
|
{
|
||||||
|
_lastColumnDefinitions = ColumnDefinitions;
|
||||||
|
MarkColumnsDirty();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private IEnumerable<T>? GetOrderedItems()
|
private IEnumerable<T>? GetOrderedItems()
|
||||||
@@ -165,39 +195,74 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
return Items.OrderByDirection(_sortDirection, sortSelector);
|
return Items.OrderByDirection(_sortDirection, sortSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IEnumerable<ColumnDefinition<T>> GetColumns()
|
protected IReadOnlyList<ColumnDefinition<T>> GetColumns()
|
||||||
{
|
{
|
||||||
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
|
if (!_columnsDirty)
|
||||||
|
{
|
||||||
|
return _visibleColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
_visibleColumns = BuildVisibleColumns();
|
||||||
|
_columnsDirty = false;
|
||||||
|
|
||||||
|
return _visibleColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
private IReadOnlyList<ColumnDefinition<T>> BuildVisibleColumns()
|
||||||
|
{
|
||||||
|
var filteredColumns = ColumnDefinitions
|
||||||
|
.Where(c => SelectedColumns.Contains(c.Id))
|
||||||
|
.Where(ColumnFilter)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
if (filteredColumns.Count == 0)
|
||||||
|
{
|
||||||
|
return EmptyColumns;
|
||||||
|
}
|
||||||
|
|
||||||
|
List<ColumnDefinition<T>> orderedColumns;
|
||||||
if (_columnOrder.Count == 0)
|
if (_columnOrder.Count == 0)
|
||||||
{
|
{
|
||||||
foreach (var column in filteredColumns)
|
orderedColumns = filteredColumns;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
if (_columnWidths.TryGetValue(column.Id, out var value))
|
var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList();
|
||||||
{
|
|
||||||
column.Width = value;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield return column;
|
|
||||||
}
|
|
||||||
|
|
||||||
yield break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
|
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
|
||||||
foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key))
|
orderedColumns = new List<ColumnDefinition<T>>(filteredColumns.Count);
|
||||||
|
|
||||||
|
foreach (var (columnId, _) in orderLookup)
|
||||||
{
|
{
|
||||||
if (!columnDictionary.TryGetValue(columnId, out var column))
|
if (!columnDictionary.TryGetValue(columnId, out var column))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
orderedColumns.Add(column);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (orderedColumns.Count != filteredColumns.Count)
|
||||||
|
{
|
||||||
|
var existingIds = new HashSet<string>(orderedColumns.Select(c => c.Id));
|
||||||
|
foreach (var column in filteredColumns)
|
||||||
|
{
|
||||||
|
if (existingIds.Add(column.Id))
|
||||||
|
{
|
||||||
|
orderedColumns.Add(column);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var column in orderedColumns)
|
||||||
|
{
|
||||||
if (_columnWidths.TryGetValue(column.Id, out var value))
|
if (_columnWidths.TryGetValue(column.Id, out var value))
|
||||||
{
|
{
|
||||||
column.Width = value;
|
column.Width = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
yield return column;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return orderedColumns;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SetSort(string columnId, SortDirection sortDirection)
|
private async Task SetSort(string columnId, SortDirection sortDirection)
|
||||||
@@ -223,6 +288,17 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
|
|
||||||
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
|
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
|
||||||
{
|
{
|
||||||
|
if (_suppressRowClickUntil is not null)
|
||||||
|
{
|
||||||
|
if (DateTimeOffset.UtcNow <= _suppressRowClickUntil.Value)
|
||||||
|
{
|
||||||
|
_suppressRowClickUntil = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_suppressRowClickUntil = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (eventArgs.Item is null)
|
if (eventArgs.Item is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -298,6 +374,7 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
|
|
||||||
protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
|
protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
|
||||||
{
|
{
|
||||||
|
_suppressRowClickUntil = DateTimeOffset.UtcNow.AddMilliseconds(500);
|
||||||
var data = _tds[columnId];
|
var data = _tds[columnId];
|
||||||
return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
|
return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
|
||||||
}
|
}
|
||||||
@@ -316,18 +393,21 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
SelectedColumns = result.SelectedColumns;
|
SelectedColumns = result.SelectedColumns;
|
||||||
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
|
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
|
||||||
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
||||||
|
MarkColumnsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
|
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
|
||||||
{
|
{
|
||||||
_columnWidths = result.ColumnWidths;
|
_columnWidths = result.ColumnWidths;
|
||||||
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
|
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
|
||||||
|
MarkColumnsDirty();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
|
if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
|
||||||
{
|
{
|
||||||
_columnOrder = result.ColumnOrder;
|
_columnOrder = result.ColumnOrder;
|
||||||
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
|
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
|
||||||
|
MarkColumnsDirty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -368,17 +448,34 @@ namespace Lantean.QBTMud.Components.UI
|
|||||||
|
|
||||||
if (column.Width.HasValue)
|
if (column.Width.HasValue)
|
||||||
{
|
{
|
||||||
className = $"overflow-cell {className}";
|
className = string.IsNullOrWhiteSpace(className)
|
||||||
|
? "overflow-cell"
|
||||||
|
: $"overflow-cell {className}";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (OnTableDataContextMenu.HasDelegate)
|
if (OnTableDataContextMenu.HasDelegate)
|
||||||
{
|
{
|
||||||
className = $"no-default-context-menu {className}";
|
className = string.IsNullOrWhiteSpace(className)
|
||||||
|
? "no-default-context-menu"
|
||||||
|
: $"no-default-context-menu {className}";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OnTableDataLongPress.HasDelegate)
|
||||||
|
{
|
||||||
|
className = string.IsNullOrWhiteSpace(className)
|
||||||
|
? "unselectable"
|
||||||
|
: $"unselectable {className}";
|
||||||
}
|
}
|
||||||
|
|
||||||
return className;
|
return className;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void MarkColumnsDirty()
|
||||||
|
{
|
||||||
|
_columnsDirty = true;
|
||||||
|
_visibleColumns = EmptyColumns;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed record SortData
|
private sealed record SortData
|
||||||
{
|
{
|
||||||
public SortData(string sortColumn, SortDirection sortDirection)
|
public SortData(string sortColumn, SortDirection sortDirection)
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
@inherits MudTd
|
@inherits MudTd
|
||||||
|
|
||||||
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
||||||
@ChildContent
|
@ChildContent
|
||||||
</td>
|
</td>
|
||||||
@@ -1,6 +1,10 @@
|
|||||||
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__body">
|
||||||
|
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
Items="WebSeeds"
|
Items="WebSeeds"
|
||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
Class="details-list" />
|
Class="details-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,6 +3,7 @@ using Lantean.QBTMud.Components.Dialogs;
|
|||||||
using Lantean.QBTMud.Filter;
|
using Lantean.QBTMud.Filter;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Helpers
|
namespace Lantean.QBTMud.Helpers
|
||||||
{
|
{
|
||||||
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
var addTorrentParams = CreateAddTorrentParams(options);
|
var addTorrentParams = CreateAddTorrentParams(options);
|
||||||
addTorrentParams.Torrents = files;
|
addTorrentParams.Torrents = files;
|
||||||
|
|
||||||
await apiClient.AddTorrent(addTorrentParams);
|
_ = await apiClient.AddTorrent(addTorrentParams);
|
||||||
|
|
||||||
foreach (var stream in streams)
|
foreach (var stream in streams)
|
||||||
{
|
{
|
||||||
@@ -74,15 +75,10 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
{
|
{
|
||||||
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
||||||
}
|
}
|
||||||
if (string.IsNullOrEmpty(options.Cookie))
|
|
||||||
{
|
|
||||||
addTorrentParams.Cookie = options.Cookie;
|
|
||||||
}
|
|
||||||
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
||||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||||
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
||||||
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
||||||
addTorrentParams.Paused = !options.StartTorrent;
|
|
||||||
addTorrentParams.RatioLimit = options.RatioLimit;
|
addTorrentParams.RatioLimit = options.RatioLimit;
|
||||||
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
||||||
addTorrentParams.SavePath = options.SavePath;
|
addTorrentParams.SavePath = options.SavePath;
|
||||||
@@ -123,7 +119,7 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
var addTorrentParams = CreateAddTorrentParams(options);
|
var addTorrentParams = CreateAddTorrentParams(options);
|
||||||
addTorrentParams.Urls = options.Urls;
|
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)
|
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;
|
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)
|
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
|
||||||
|
|||||||
@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
{
|
{
|
||||||
if (seconds is null)
|
if (seconds is null)
|
||||||
{
|
{
|
||||||
return "";
|
return string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seconds == 8640000)
|
const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
|
||||||
|
var value = seconds.Value;
|
||||||
|
|
||||||
|
if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
|
||||||
{
|
{
|
||||||
return "∞";
|
return "∞";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (seconds < 60)
|
if (value <= 0)
|
||||||
{
|
{
|
||||||
return "< 1m";
|
return "< 1m";
|
||||||
}
|
}
|
||||||
|
|
||||||
TimeSpan time;
|
var time = TimeSpan.FromSeconds(value);
|
||||||
try
|
if (time.TotalMinutes < 1)
|
||||||
{
|
{
|
||||||
time = TimeSpan.FromSeconds(seconds.Value);
|
return "< 1m";
|
||||||
}
|
|
||||||
catch (OverflowException)
|
|
||||||
{
|
|
||||||
return "∞";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb = new StringBuilder();
|
var sb = new StringBuilder();
|
||||||
if (prefix is not null)
|
if (prefix is not null)
|
||||||
{
|
{
|
||||||
@@ -129,7 +129,7 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
return Size(size);
|
return Size(size, prefix, suffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -404,8 +404,6 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
|
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
|
||||||
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
|
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
|
||||||
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
|
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.Stopped => (Icons.Material.Filled.Stop, Color.Default),
|
||||||
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
|
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
|
||||||
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
|
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
|
||||||
|
|||||||
40
Lantean.QBTMud/Helpers/EventArgsExtensions.cs
Normal file
40
Lantean.QBTMud/Helpers/EventArgsExtensions.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Helpers
|
||||||
|
{
|
||||||
|
public static class EventArgsExtensions
|
||||||
|
{
|
||||||
|
public static EventArgs NormalizeForContextMenu(this EventArgs eventArgs)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(eventArgs);
|
||||||
|
|
||||||
|
if (eventArgs is LongPressEventArgs longPressEventArgs)
|
||||||
|
{
|
||||||
|
return longPressEventArgs.ToMouseEventArgs();
|
||||||
|
}
|
||||||
|
|
||||||
|
return eventArgs;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static MouseEventArgs ToMouseEventArgs(this LongPressEventArgs longPressEventArgs)
|
||||||
|
{
|
||||||
|
ArgumentNullException.ThrowIfNull(longPressEventArgs);
|
||||||
|
|
||||||
|
return new MouseEventArgs
|
||||||
|
{
|
||||||
|
Button = 2,
|
||||||
|
Buttons = 2,
|
||||||
|
ClientX = longPressEventArgs.ClientX,
|
||||||
|
ClientY = longPressEventArgs.ClientY,
|
||||||
|
OffsetX = longPressEventArgs.OffsetX,
|
||||||
|
OffsetY = longPressEventArgs.OffsetY,
|
||||||
|
PageX = longPressEventArgs.PageX,
|
||||||
|
PageY = longPressEventArgs.PageY,
|
||||||
|
ScreenX = longPressEventArgs.ScreenX,
|
||||||
|
ScreenY = longPressEventArgs.ScreenY,
|
||||||
|
Type = longPressEventArgs.Type ?? "contextmenu",
|
||||||
|
Detail = -1,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -119,34 +119,35 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
switch (category)
|
switch (category)
|
||||||
{
|
{
|
||||||
case CATEGORY_ALL:
|
case CATEGORY_ALL:
|
||||||
break;
|
return true;
|
||||||
|
|
||||||
case CATEGORY_UNCATEGORIZED:
|
case CATEGORY_UNCATEGORIZED:
|
||||||
if (!string.IsNullOrEmpty(torrent.Category))
|
if (!string.IsNullOrEmpty(torrent.Category))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
if (!useSubcategories)
|
|
||||||
{
|
|
||||||
if (torrent.Category != category)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!torrent.Category.StartsWith(category))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
if (string.IsNullOrEmpty(torrent.Category))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!useSubcategories)
|
||||||
|
{
|
||||||
|
return string.Equals(torrent.Category, category, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.Equals(torrent.Category, category, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var prefix = string.Concat(category, "/");
|
||||||
|
return torrent.Category.StartsWith(prefix, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool FilterTag(Torrent torrent, string tag)
|
public static bool FilterTag(Torrent torrent, string tag)
|
||||||
@@ -199,15 +200,8 @@ namespace Lantean.QBTMud.Helpers
|
|||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Status.Resumed:
|
case Status.Stopped:
|
||||||
if (!state.Contains("resumed"))
|
if (state != "stoppedDL" && state != "stoppedUP")
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case Status.Paused:
|
|
||||||
if (!state.Contains("paused") || !state.Contains("stopped"))
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,13 +12,11 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
|
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
|
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
||||||
<PackageReference Include="MudBlazor" Version="8.2.0" />
|
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
||||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
||||||
<!-- added to fix vuln in dependency -->
|
|
||||||
<PackageReference Include="System.Text.Json" Version="9.0.1" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@layout LoggedInLayout
|
@layout LoggedInLayout
|
||||||
|
|
||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
<div class="app-shell__body">
|
||||||
|
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
|
||||||
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
|
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
<MudMainContent>
|
<MudMainContent Class="app-shell__main">
|
||||||
@Body
|
@Body
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
|
</div>
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@layout LoggedInLayout
|
@layout LoggedInLayout
|
||||||
|
|
||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
<div class="app-shell__body">
|
||||||
|
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
|
||||||
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
|
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
<MudMainContent>
|
<MudMainContent Class="app-shell__main">
|
||||||
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
||||||
@Body
|
@Body
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
|
</div>
|
||||||
@@ -10,6 +10,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<CascadingValue Value="Torrents">
|
<CascadingValue Value="Torrents">
|
||||||
|
<CascadingValue Value="_torrentsVersion" Name="TorrentsVersion">
|
||||||
<CascadingValue Value="MainData">
|
<CascadingValue Value="MainData">
|
||||||
<CascadingValue Value="Preferences">
|
<CascadingValue Value="Preferences">
|
||||||
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
|
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
|
||||||
@@ -23,20 +24,9 @@
|
|||||||
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
||||||
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
|
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
|
||||||
<CascadingValue Value="Version" Name="Version">
|
<CascadingValue Value="Version" Name="Version">
|
||||||
|
<div class="app-shell">
|
||||||
@Body
|
@Body
|
||||||
</CascadingValue>
|
<MudAppBar Bottom="true" Elevation="0" Dense="true" Class="app-shell__status-bar">
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
|
|
||||||
@if (MainData?.LostConnection == true)
|
@if (MainData?.LostConnection == true)
|
||||||
{
|
{
|
||||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
||||||
@@ -49,7 +39,7 @@
|
|||||||
@{
|
@{
|
||||||
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
||||||
}
|
}
|
||||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
|
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="@MainData?.ServerState.ConnectionStatus" />
|
||||||
<MudDivider Vertical="true" Class="" />
|
<MudDivider Vertical="true" Class="" />
|
||||||
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
||||||
<MudDivider Vertical="true" Class="" />
|
<MudDivider Vertical="true" Class="" />
|
||||||
@@ -65,5 +55,19 @@
|
|||||||
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
|
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
|
||||||
</MudText>
|
</MudText>
|
||||||
</MudAppBar>
|
</MudAppBar>
|
||||||
|
</div>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
|
</CascadingValue>
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
@@ -52,22 +52,36 @@ namespace Lantean.QBTMud.Layout
|
|||||||
|
|
||||||
protected string? SearchText { get; set; }
|
protected string? SearchText { get; set; }
|
||||||
|
|
||||||
protected IEnumerable<Torrent> Torrents => GetTorrents();
|
protected IReadOnlyList<Torrent> Torrents => GetTorrents();
|
||||||
|
|
||||||
protected bool IsAuthenticated { get; set; }
|
protected bool IsAuthenticated { get; set; }
|
||||||
|
|
||||||
protected bool LostConnection { get; set; }
|
protected bool LostConnection { get; set; }
|
||||||
|
|
||||||
private List<Torrent> GetTorrents()
|
private IReadOnlyList<Torrent> _visibleTorrents = Array.Empty<Torrent>();
|
||||||
|
|
||||||
|
private bool _torrentsDirty = true;
|
||||||
|
private int _torrentsVersion;
|
||||||
|
|
||||||
|
private IReadOnlyList<Torrent> GetTorrents()
|
||||||
{
|
{
|
||||||
|
if (!_torrentsDirty)
|
||||||
|
{
|
||||||
|
return _visibleTorrents;
|
||||||
|
}
|
||||||
|
|
||||||
if (MainData is null)
|
if (MainData is null)
|
||||||
{
|
{
|
||||||
return [];
|
_visibleTorrents = Array.Empty<Torrent>();
|
||||||
|
_torrentsDirty = false;
|
||||||
|
return _visibleTorrents;
|
||||||
}
|
}
|
||||||
|
|
||||||
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
|
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
|
||||||
|
_visibleTorrents = MainData.Torrents.Values.Filter(filterState).ToList();
|
||||||
|
_torrentsDirty = false;
|
||||||
|
|
||||||
return MainData.Torrents.Values.Filter(filterState).ToList();
|
return _visibleTorrents;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -84,6 +98,7 @@ namespace Lantean.QBTMud.Layout
|
|||||||
Version = await ApiClient.GetApplicationVersion();
|
Version = await ApiClient.GetApplicationVersion();
|
||||||
var data = await ApiClient.GetMainData(_requestId);
|
var data = await ApiClient.GetMainData(_requestId);
|
||||||
MainData = DataManager.CreateMainData(data);
|
MainData = DataManager.CreateMainData(data);
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
|
||||||
_requestId = data.ResponseId;
|
_requestId = data.ResponseId;
|
||||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||||
@@ -126,32 +141,51 @@ namespace Lantean.QBTMud.Layout
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldRender = false;
|
||||||
|
|
||||||
if (MainData is null || data.FullUpdate)
|
if (MainData is null || data.FullUpdate)
|
||||||
{
|
{
|
||||||
MainData = DataManager.CreateMainData(data);
|
MainData = DataManager.CreateMainData(data);
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
shouldRender = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeMainData(data, MainData);
|
var dataChanged = DataManager.MergeMainData(data, MainData, out var filterChanged);
|
||||||
|
if (filterChanged)
|
||||||
|
{
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
else if (dataChanged)
|
||||||
|
{
|
||||||
|
IncrementTorrentsVersion();
|
||||||
|
}
|
||||||
|
shouldRender = dataChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (MainData is not null)
|
||||||
|
{
|
||||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||||
|
}
|
||||||
_requestId = data.ResponseId;
|
_requestId = data.ResponseId;
|
||||||
|
if (shouldRender)
|
||||||
|
{
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
|
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, OnCategoryChanged);
|
||||||
|
|
||||||
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
|
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, OnStatusChanged);
|
||||||
|
|
||||||
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
|
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, OnTagChanged);
|
||||||
|
|
||||||
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
|
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, OnTrackerChanged);
|
||||||
|
|
||||||
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
|
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, OnSearchTermChanged);
|
||||||
|
|
||||||
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
|
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
|
||||||
|
|
||||||
@@ -159,12 +193,81 @@ namespace Lantean.QBTMud.Layout
|
|||||||
|
|
||||||
protected static (string, Color) GetConnectionIcon(string? status)
|
protected static (string, Color) GetConnectionIcon(string? status)
|
||||||
{
|
{
|
||||||
if (status is null)
|
return status switch
|
||||||
{
|
{
|
||||||
return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
|
"firewalled" => (Icons.Material.Outlined.SignalWifiStatusbarConnectedNoInternet4, Color.Warning),
|
||||||
|
"connected" => (Icons.Material.Outlined.SignalWifi4Bar, Color.Success),
|
||||||
|
_ => (Icons.Material.Outlined.SignalWifiOff, Color.Error),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
|
private void OnCategoryChanged(string category)
|
||||||
|
{
|
||||||
|
if (Category == category)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Category = category;
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnStatusChanged(Status status)
|
||||||
|
{
|
||||||
|
if (Status == status)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Status = status;
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTagChanged(string tag)
|
||||||
|
{
|
||||||
|
if (Tag == tag)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tag = tag;
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTrackerChanged(string tracker)
|
||||||
|
{
|
||||||
|
if (Tracker == tracker)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Tracker = tracker;
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnSearchTermChanged(string term)
|
||||||
|
{
|
||||||
|
if (SearchText == term)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchText = term;
|
||||||
|
MarkTorrentsDirty();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void MarkTorrentsDirty()
|
||||||
|
{
|
||||||
|
_torrentsDirty = true;
|
||||||
|
IncrementTorrentsVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void IncrementTorrentsVersion()
|
||||||
|
{
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
_torrentsVersion++;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
protected virtual void Dispose(bool disposing)
|
||||||
|
|||||||
@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
|
|||||||
|
|
||||||
private bool _disposedValue;
|
private bool _disposedValue;
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
|
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
|
||||||
|
|
||||||
@@ -78,13 +75,13 @@ namespace Lantean.QBTMud.Layout
|
|||||||
{
|
{
|
||||||
IsDarkMode = isDarkMode.Value;
|
IsDarkMode = isDarkMode.Value;
|
||||||
}
|
}
|
||||||
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
|
||||||
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected Task OnSystemPreferenceChanged(bool value)
|
protected Task OnSystemDarkModeChanged(bool value)
|
||||||
{
|
{
|
||||||
IsDarkMode = value;
|
IsDarkMode = value;
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
|
|||||||
@@ -1,11 +1,13 @@
|
|||||||
@inherits LayoutComponentBase
|
@inherits LayoutComponentBase
|
||||||
@layout LoggedInLayout
|
@layout LoggedInLayout
|
||||||
|
|
||||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
<div class="app-shell__body">
|
||||||
|
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
|
||||||
<MudNavMenu>
|
<MudNavMenu>
|
||||||
<ApplicationActions IsMenu="false" Preferences="Preferences" />
|
<ApplicationActions IsMenu="false" Preferences="Preferences" />
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
</MudDrawer>
|
</MudDrawer>
|
||||||
<MudMainContent>
|
<MudMainContent Class="app-shell__main">
|
||||||
@Body
|
@Body
|
||||||
</MudMainContent>
|
</MudMainContent>
|
||||||
|
</div>
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
Downloading,
|
Downloading,
|
||||||
Seeding,
|
Seeding,
|
||||||
Completed,
|
Completed,
|
||||||
Resumed,
|
|
||||||
Paused,
|
Paused,
|
||||||
|
Stopped,
|
||||||
Active,
|
Active,
|
||||||
Inactive,
|
Inactive,
|
||||||
Stalled,
|
Stalled,
|
||||||
@@ -15,6 +15,5 @@
|
|||||||
StalledDownloading,
|
StalledDownloading,
|
||||||
Checking,
|
Checking,
|
||||||
Errored,
|
Errored,
|
||||||
Stopped
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
namespace Lantean.QBTMud.Models
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Models
|
|
||||||
{
|
{
|
||||||
public record TorrentOptions
|
public record TorrentOptions
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/about"
|
@page "/about"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -9,10 +11,12 @@
|
|||||||
}
|
}
|
||||||
<MudText Class="px-5 no-wrap">About</MudText>
|
<MudText Class="px-5 no-wrap">About</MudText>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
|
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
|
||||||
<MudTabPanel Text="About">
|
<MudTabPanel Text="About">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 content-panel__container options-tab-contents">
|
||||||
<MudGrid Class="mt-0 mb-4">
|
<MudGrid Class="mt-0 mb-4">
|
||||||
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
|
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
|
||||||
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
|
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
|
||||||
@@ -60,7 +64,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Authors">
|
<MudTabPanel Text="Authors">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
|
||||||
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
|
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
|
||||||
|
|
||||||
<MudGrid Class="mt-0 mb-4">
|
<MudGrid Class="mt-0 mb-4">
|
||||||
@@ -108,7 +112,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Special Thanks">
|
<MudTabPanel Text="Special Thanks">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
|
||||||
<MudText Typo="Typo.body1" Class="py-1">I would first like to thank sourceforge.net for hosting qBittorrent project and for their support.</MudText>
|
<MudText Typo="Typo.body1" Class="py-1">I would first like to thank sourceforge.net for hosting qBittorrent project and for their support.</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="py-1">I am pleased that people from all over the world are contributing to qBittorrent: Ishan Arora (India), Arnaud Demaizière (France) and Stephanos Antaris (Greece). Their help is greatly appreciated</MudText>
|
<MudText Typo="Typo.body1" Class="py-1">I am pleased that people from all over the world are contributing to qBittorrent: Ishan Arora (India), Arnaud Demaizière (France) and Stephanos Antaris (Greece). Their help is greatly appreciated</MudText>
|
||||||
<MudText Typo="Typo.body1" Class="py-1">I also want to thank Στέφανος Αντάρης (santaris@csd.auth.gr) and Mirco Chinelli (infinity89@fastwebmail.it) for working on Mac OS X packaging.</MudText>
|
<MudText Typo="Typo.body1" Class="py-1">I also want to thank Στέφανος Αντάρης (santaris@csd.auth.gr) and Mirco Chinelli (infinity89@fastwebmail.it) for working on Mac OS X packaging.</MudText>
|
||||||
@@ -118,7 +122,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Translators">
|
<MudTabPanel Text="Translators">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
|
||||||
<MudText Typo="Typo.body1" Class="py-1">
|
<MudText Typo="Typo.body1" Class="py-1">
|
||||||
I would like to thank the people who volunteered to Circle qBittorrent.<br>
|
I would like to thank the people who volunteered to Circle qBittorrent.<br>
|
||||||
Most of them Circled via <MudLink Target="https://www.transifex.com/sledgehammer999/qbittorrent/" Href="https://www.transifex.com/sledgehammer999/qbittorrent/">Transifex</MudLink> and some of them are mentioned below:<br>
|
Most of them Circled via <MudLink Target="https://www.transifex.com/sledgehammer999/qbittorrent/" Href="https://www.transifex.com/sledgehammer999/qbittorrent/">Transifex</MudLink> and some of them are mentioned below:<br>
|
||||||
@@ -168,7 +172,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Licence">
|
<MudTabPanel Text="Licence">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
|
||||||
<MudText Typo="Typo.body1" Class="py-1">
|
<MudText Typo="Typo.body1" Class="py-1">
|
||||||
The qBittorrent source code is licensed under the GNU General Public License, version 2 or (at your option) any later version (GPLv2+).
|
The qBittorrent source code is licensed under the GNU General Public License, version 2 or (at your option) any later version (GPLv2+).
|
||||||
However, this binary distribution is licensed under GNU General Public License, version 3 or (at your option) any later version (GPLv3+),
|
However, this binary distribution is licensed under GNU General Public License, version 3 or (at your option) any later version (GPLv3+),
|
||||||
@@ -1061,7 +1065,7 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Software Used">
|
<MudTabPanel Text="Software Used">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3 options-tab-contents">
|
||||||
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
|
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
|
||||||
|
|
||||||
<MudGrid Class="mt-1 mb-4">
|
<MudGrid Class="mt-1 mb-4">
|
||||||
@@ -1105,3 +1109,5 @@
|
|||||||
</MudContainer>
|
</MudContainer>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/blocks"
|
@page "/blocks"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -10,7 +12,8 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
|
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<EditForm Model="Model" OnSubmit="Submit">
|
<EditForm Model="Model" OnSubmit="Submit">
|
||||||
@@ -33,4 +36,6 @@
|
|||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
RowClassFunc="RowClass"
|
RowClassFunc="RowClass"
|
||||||
Class="search-list" />
|
Class="search-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/categories"
|
@page "/categories"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -11,14 +13,18 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
|
<MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<DynamicTable @ref="Table"
|
<DynamicTable @ref="Table"
|
||||||
T="Category"
|
T="Category"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
Items="Results"
|
Items="Results"
|
||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
Class="details-list" />
|
Class="details-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private RenderFragment<RowContext<Category>> ActionsColumn
|
private RenderFragment<RowContext<Category>> ActionsColumn
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
@page "/details/{hash}"
|
@page "/details/{hash}"
|
||||||
@layout DetailsLayout
|
@layout DetailsLayout
|
||||||
|
|
||||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar content-panel__toolbar--scroll">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -17,6 +18,7 @@
|
|||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
@if (ShowTabs)
|
@if (ShowTabs)
|
||||||
{
|
{
|
||||||
<CascadingValue Value="RefreshInterval" Name="RefreshInterval">
|
<CascadingValue Value="RefreshInterval" Name="RefreshInterval">
|
||||||
@@ -39,3 +41,5 @@
|
|||||||
</MudTabs>
|
</MudTabs>
|
||||||
</CascadingValue>
|
</CascadingValue>
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/log"
|
@page "/log"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -10,7 +12,8 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudText Class="pl-5 no-wrap">Execution Log</MudText>
|
<MudText Class="pl-5 no-wrap">Execution Log</MudText>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<EditForm Model="Model" OnSubmit="Submit">
|
<EditForm Model="Model" OnSubmit="Submit">
|
||||||
@@ -41,4 +44,6 @@
|
|||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
RowClassFunc="RowClass"
|
RowClassFunc="RowClass"
|
||||||
Class="search-list" />
|
Class="search-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
|
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -14,30 +16,50 @@
|
|||||||
<MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
<MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
<MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
|
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
|
||||||
<MudTabPanel Text="Behaviour">
|
<MudTabPanel Text="Behaviour">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Downloads">
|
<MudTabPanel Text="Downloads">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Connection">
|
<MudTabPanel Text="Connection">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Speed">
|
<MudTabPanel Text="Speed">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="BitTorrent">
|
<MudTabPanel Text="BitTorrent">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="RSS">
|
<MudTabPanel Text="RSS">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Web UI">
|
<MudTabPanel Text="Web UI">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
<MudTabPanel Text="Advanced">
|
<MudTabPanel Text="Advanced">
|
||||||
|
<div class="options-tab-contents">
|
||||||
<AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
<AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||||
|
</div>
|
||||||
</MudTabPanel>
|
</MudTabPanel>
|
||||||
</MudTabs>
|
</MudTabs>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/rss"
|
@page "/rss"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -15,8 +17,10 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
|
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
|
<div class="content-panel__body">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="content-panel__container">
|
||||||
<MudGrid Class="rss-contents">
|
<MudGrid Class="rss-contents">
|
||||||
<MudItem xs="4" Style="height: 100%">
|
<MudItem xs="4" Style="height: 100%">
|
||||||
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
|
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
|
||||||
@@ -71,3 +75,5 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/search"
|
@page "/search"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -10,7 +12,8 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudText Class="pl-5 no-wrap">Search</MudText>
|
<MudText Class="pl-5 no-wrap">Search</MudText>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
<MudCardContent>
|
<MudCardContent>
|
||||||
<EditForm Model="Model" OnValidSubmit="DoSearch">
|
<EditForm Model="Model" OnValidSubmit="DoSearch">
|
||||||
@@ -59,4 +62,6 @@
|
|||||||
Items="Results"
|
Items="Results"
|
||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
Class="search-list" />
|
Class="search-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/statistics"
|
@page "/statistics"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -10,8 +12,10 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudText Class="pl-5 no-wrap">Statistics</MudText>
|
<MudText Class="pl-5 no-wrap">Statistics</MudText>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents">
|
<div class="content-panel__body">
|
||||||
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents content-panel__container">
|
||||||
<MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
|
<MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
@@ -60,3 +64,5 @@
|
|||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
@page "/tags"
|
@page "/tags"
|
||||||
@layout OtherLayout
|
@layout OtherLayout
|
||||||
|
|
||||||
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
@if (!DrawerOpen)
|
@if (!DrawerOpen)
|
||||||
{
|
{
|
||||||
@@ -11,14 +13,18 @@
|
|||||||
<MudDivider Vertical="true" />
|
<MudDivider Vertical="true" />
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
|
<MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="content-panel__body">
|
||||||
<DynamicTable @ref="Table"
|
<DynamicTable @ref="Table"
|
||||||
T="string"
|
T="string"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
Items="Results"
|
Items="Results"
|
||||||
MultiSelection="false"
|
MultiSelection="false"
|
||||||
SelectOnRowClick="false"
|
SelectOnRowClick="false"
|
||||||
Class="details-list" />
|
Class="details-list content-panel__table" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private RenderFragment<RowContext<string>> ActionsColumn
|
private RenderFragment<RowContext<string>> ActionsColumn
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
@page "/"
|
@page "/"
|
||||||
@layout ListLayout
|
@layout ListLayout
|
||||||
|
|
||||||
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
|
<MudMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
||||||
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
|
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
|
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
|
||||||
</ContextMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
<div class="content-panel">
|
||||||
|
<div class="content-panel__toolbar content-panel__toolbar--scroll">
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
<MudToolBar Gutters="false" Dense="true">
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
|
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
|
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
|
||||||
@@ -20,12 +21,12 @@
|
|||||||
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||||
</MudToolBar>
|
</MudToolBar>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="content-panel__body">
|
||||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
|
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0 content-panel__container">
|
||||||
<DynamicTable
|
<DynamicTable
|
||||||
@ref="Table"
|
@ref="Table"
|
||||||
T="Torrent"
|
T="Torrent"
|
||||||
Class="torrent-list"
|
Class="torrent-list content-panel__table"
|
||||||
ColumnDefinitions="Columns"
|
ColumnDefinitions="Columns"
|
||||||
Items="Torrents"
|
Items="Torrents"
|
||||||
OnRowClick="RowClick"
|
OnRowClick="RowClick"
|
||||||
@@ -38,6 +39,8 @@
|
|||||||
OnTableDataLongPress="TableDataLongPress"
|
OnTableDataLongPress="TableDataLongPress"
|
||||||
/>
|
/>
|
||||||
</MudContainer>
|
</MudContainer>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
|
private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
|
||||||
|
|||||||
@@ -35,11 +35,17 @@ namespace Lantean.QBTMud.Pages
|
|||||||
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
|
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public IEnumerable<Torrent>? Torrents { get; set; }
|
public IReadOnlyList<Torrent>? Torrents { get; set; }
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MainData MainData { get; set; } = default!;
|
public MainData MainData { get; set; } = default!;
|
||||||
|
|
||||||
|
[CascadingParameter(Name = "LostConnection")]
|
||||||
|
public bool LostConnection { get; set; }
|
||||||
|
|
||||||
|
[CascadingParameter(Name = "TorrentsVersion")]
|
||||||
|
public int TorrentsVersion { get; set; }
|
||||||
|
|
||||||
[CascadingParameter(Name = "SearchTermChanged")]
|
[CascadingParameter(Name = "SearchTermChanged")]
|
||||||
public EventCallback<string> SearchTermChanged { get; set; }
|
public EventCallback<string> SearchTermChanged { get; set; }
|
||||||
|
|
||||||
@@ -56,13 +62,23 @@ namespace Lantean.QBTMud.Pages
|
|||||||
|
|
||||||
protected HashSet<Torrent> SelectedItems { get; set; } = [];
|
protected HashSet<Torrent> SelectedItems { get; set; } = [];
|
||||||
|
|
||||||
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
|
protected bool ToolbarButtonsEnabled => _toolbarButtonsEnabled;
|
||||||
|
|
||||||
protected DynamicTable<Torrent>? Table { get; set; }
|
protected DynamicTable<Torrent>? Table { get; set; }
|
||||||
|
|
||||||
protected Torrent? ContextMenuItem { get; set; }
|
protected Torrent? ContextMenuItem { get; set; }
|
||||||
|
|
||||||
protected ContextMenu? ContextMenu { get; set; }
|
protected MudMenu? ContextMenu { get; set; }
|
||||||
|
|
||||||
|
private object? _lastRenderedTorrents;
|
||||||
|
private QBitTorrentClient.Models.Preferences? _lastPreferences;
|
||||||
|
private bool _lastLostConnection;
|
||||||
|
private bool _hasRendered;
|
||||||
|
private int _lastSelectionCount;
|
||||||
|
private int _lastTorrentsVersion = -1;
|
||||||
|
private bool _pendingSelectionChange;
|
||||||
|
|
||||||
|
private bool _toolbarButtonsEnabled;
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
{
|
{
|
||||||
@@ -73,9 +89,81 @@ namespace Lantean.QBTMud.Pages
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected override bool ShouldRender()
|
||||||
|
{
|
||||||
|
if (!_hasRendered)
|
||||||
|
{
|
||||||
|
_hasRendered = true;
|
||||||
|
_lastRenderedTorrents = Torrents;
|
||||||
|
_lastPreferences = Preferences;
|
||||||
|
_lastLostConnection = LostConnection;
|
||||||
|
_lastTorrentsVersion = TorrentsVersion;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_pendingSelectionChange)
|
||||||
|
{
|
||||||
|
_pendingSelectionChange = false;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastTorrentsVersion != TorrentsVersion)
|
||||||
|
{
|
||||||
|
_lastTorrentsVersion = TorrentsVersion;
|
||||||
|
_lastRenderedTorrents = Torrents;
|
||||||
|
_lastPreferences = Preferences;
|
||||||
|
_lastLostConnection = LostConnection;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReferenceEquals(_lastRenderedTorrents, Torrents))
|
||||||
|
{
|
||||||
|
_lastRenderedTorrents = Torrents;
|
||||||
|
_lastPreferences = Preferences;
|
||||||
|
_lastLostConnection = LostConnection;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ReferenceEquals(_lastPreferences, Preferences))
|
||||||
|
{
|
||||||
|
_lastPreferences = Preferences;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastLostConnection != LostConnection)
|
||||||
|
{
|
||||||
|
_lastLostConnection = LostConnection;
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_lastSelectionCount != SelectedItems.Count)
|
||||||
|
{
|
||||||
|
_lastSelectionCount = SelectedItems.Count;
|
||||||
|
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
|
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
|
||||||
{
|
{
|
||||||
SelectedItems = selectedItems;
|
SelectedItems = selectedItems;
|
||||||
|
_toolbarButtonsEnabled = SelectedItems.Count > 0;
|
||||||
|
_pendingSelectionChange = true;
|
||||||
|
InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
|
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
|
||||||
@@ -185,7 +273,9 @@ namespace Lantean.QBTMud.Pages
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await ContextMenu.ToggleMenuAsync(eventArgs);
|
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
|
||||||
|
|
||||||
|
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
|
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Services;
|
using Lantean.QBTMud.Services;
|
||||||
using Microsoft.AspNetCore.Components.Web;
|
using Microsoft.AspNetCore.Components.Web;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
|
|
||||||
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
||||||
|
|
||||||
void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList);
|
bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
|
||||||
|
|
||||||
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
||||||
|
|
||||||
@@ -16,7 +16,7 @@ namespace Lantean.QBTMud.Services
|
|||||||
|
|
||||||
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
||||||
|
|
||||||
void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
||||||
|
|
||||||
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
||||||
|
|
||||||
|
|||||||
@@ -65,15 +65,11 @@ code {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.mud-appbar.mud-appbar-fixed-bottom {
|
.mud-appbar.mud-appbar-fixed-bottom {
|
||||||
height: 35px;
|
height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
|
||||||
}
|
|
||||||
|
|
||||||
.mud-main-content {
|
|
||||||
padding-bottom: 35px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.mud-drawer-fixed.mud-drawer-mini.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-persistent:not(.mud-drawer-clipped-never), .mud-drawer-fixed.mud-drawer-responsive.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-temporary.mud-drawer-clipped-always {
|
.mud-drawer-fixed.mud-drawer-mini.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-persistent:not(.mud-drawer-clipped-never), .mud-drawer-fixed.mud-drawer-responsive.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-temporary.mud-drawer-clipped-always {
|
||||||
height: calc(100% - var(--mud-appbar-height) - 35px);
|
height: calc(100% - var(--mud-appbar-height) - (var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px)));
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-100 {
|
.w-100 {
|
||||||
@@ -154,25 +150,91 @@ code {
|
|||||||
margin-right: 5px;
|
margin-right: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.torrent-list .mud-table-container {
|
/*. Layout helpers */
|
||||||
height: calc(100vh - 160px);
|
.content-panel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: 100%;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.file-list .mud-table-container {
|
.content-panel__toolbar {
|
||||||
height: calc(100vh - 245px);
|
flex: 0 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-list .mud-table-container {
|
.content-panel__toolbar--scroll {
|
||||||
height: calc(100vh - 200px);
|
overflow-x: auto;
|
||||||
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.details-tab-contents {
|
.content-panel__body {
|
||||||
height: calc(100vh - 200px);
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__table {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__table .mud-table-container {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__body > .mud-tabs {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__body > .mud-tabs .mud-tabs-tabbar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__body > .mud-tabs .mud-tabs-panels {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
padding-top: 0;
|
||||||
|
margin-top: -1px;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__body .mud-tabs .mud-tabs-panels .mud-tab-panel {
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.torrent-list .mud-table-container,
|
||||||
|
.file-list .mud-table-container,
|
||||||
|
.details-list .mud-table-container,
|
||||||
.search-list .mud-table-container {
|
.search-list .mud-table-container {
|
||||||
height: calc(100vh - 260px);
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.details-tab-contents,
|
||||||
|
.options-tab-contents,
|
||||||
|
.rss-contents {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
tr.log-normal td {
|
tr.log-normal td {
|
||||||
@@ -220,10 +282,6 @@ td .folder-button {
|
|||||||
padding: 6px 16px 6px 16px !important;
|
padding: 6px 16px 6px 16px !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.rss-contents {
|
|
||||||
height: calc(100vh - 149px);
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
@keyframes spin {
|
||||||
0% {
|
0% {
|
||||||
transform: rotate(0deg);
|
transform: rotate(0deg);
|
||||||
@@ -256,3 +314,116 @@ td .folder-button {
|
|||||||
.mud-popover .mud-divider:last-child {
|
.mud-popover .mud-divider:last-child {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
:root {
|
||||||
|
--app-viewport-height: 100vh;
|
||||||
|
--app-status-bar-height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (height: 100svh) {
|
||||||
|
:root {
|
||||||
|
--app-viewport-height: 100svh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports ((height: 100dvh) and (not (height: 100svh))) {
|
||||||
|
:root {
|
||||||
|
--app-viewport-height: 100dvh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: var(--app-viewport-height);
|
||||||
|
min-height: var(--app-viewport-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
overscroll-behavior: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app,
|
||||||
|
.mud-layout {
|
||||||
|
height: 100%;
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
height: var(--app-viewport-height);
|
||||||
|
min-height: var(--app-viewport-height);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__body {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__sidebar {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__main {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
padding: var(--mud-appbar-height) 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__status-bar.mud-appbar {
|
||||||
|
flex: 0 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
|
||||||
|
height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
|
||||||
|
width: 100%;
|
||||||
|
background-color: var(--mud-palette-dark-lighten);
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-start;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell__status-bar .mud-toolbar {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
padding-bottom: env(safe-area-inset-bottom, 0px);
|
||||||
|
background-color: inherit;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
@supports (-webkit-touch-callout: none) {
|
||||||
|
:root {
|
||||||
|
--app-viewport-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
|
||||||
|
.app-shell {
|
||||||
|
height: -webkit-fill-available;
|
||||||
|
min-height: -webkit-fill-available;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Tab bar gap fix */
|
||||||
|
.content-panel__body > .mud-tabs .mud-tabs-tabbar {
|
||||||
|
margin-bottom: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
|
border-bottom-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-wrapper {
|
||||||
|
margin-bottom: -1px;
|
||||||
|
}
|
||||||
|
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-slider {
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|||||||
@@ -9,12 +9,12 @@
|
|||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
|
||||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
<link href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||||
<link rel="stylesheet" href="css/app.css" />
|
<link rel="stylesheet" href="./css/app.css" />
|
||||||
<link rel="icon" type="image/png" href="images/qbittorrent32.png" />
|
<link rel="icon" type="image/png" href="images/qbittorrent32.png" />
|
||||||
<link rel="icon" href="images/qbittorrent-tray.svg">
|
<link rel="icon" href="./images/qbittorrent-tray.svg">
|
||||||
<link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000">
|
<link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
|
||||||
<link rel="apple-touch-icon" href="images/qbittorrent32.png">
|
<link rel="apple-touch-icon" href="./images/qbittorrent32.png">
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
@@ -31,10 +31,10 @@
|
|||||||
<a href="" class="reload">Reload</a>
|
<a href="" class="reload">Reload</a>
|
||||||
<a class="dismiss">🗙</a>
|
<a class="dismiss">🗙</a>
|
||||||
</div>
|
</div>
|
||||||
<script src="_framework/blazor.webassembly.js"></script>
|
<script src="./_framework/blazor.webassembly.js"></script>
|
||||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
<script src="./_content/MudBlazor/MudBlazor.min.js"></script>
|
||||||
<script src="js/piecesbar.js"></script>
|
<script src="./js/piecesbar.js"></script>
|
||||||
<script src="js/interop.js"></script>
|
<script src="./js/interop.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
@@ -5,4 +5,4 @@
|
|||||||
// * @author John Doherty <www.johndoherty.info>
|
// * @author John Doherty <www.johndoherty.info>
|
||||||
// * @license MIT
|
// * @license MIT
|
||||||
// */
|
// */
|
||||||
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e), this.dispatchEvent(new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY })) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
|
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e); var n = new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY }); n.__longPress = !0, this.dispatchEvent(n) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
|
||||||
@@ -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>
|
||||||
11
Lantean.QBitTorrentClient.Test/UnitTest1.cs
Normal file
11
Lantean.QBitTorrentClient.Test/UnitTest1.cs
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
namespace Lantean.QBitTorrentClient.Test
|
||||||
|
{
|
||||||
|
public class UnitTest1
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Test1()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,9 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
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;
|
||||||
using System.Net.Http.Json;
|
using System.Net.Http.Json;
|
||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
@@ -105,6 +110,8 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
public async Task SetApplicationPreferences(UpdatePreferences preferences)
|
public async Task SetApplicationPreferences(UpdatePreferences preferences)
|
||||||
{
|
{
|
||||||
|
preferences.Validate();
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(preferences, _options);
|
var json = JsonSerializer.Serialize(preferences, _options);
|
||||||
|
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -116,6 +123,49 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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()
|
public async Task<string> GetDefaultSavePath()
|
||||||
{
|
{
|
||||||
var response = await _httpClient.GetAsync("app/defaultSavePath");
|
var response = await _httpClient.GetAsync("app/defaultSavePath");
|
||||||
@@ -145,6 +195,43 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#endregion Application
|
#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
|
#region Log
|
||||||
|
|
||||||
public async Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null)
|
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
|
#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();
|
var query = new QueryBuilder();
|
||||||
if (filter is not null)
|
if (filter is not null)
|
||||||
@@ -344,6 +431,10 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
query.Add("private", isPrivate.Value ? "true" : "false");
|
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);
|
var response = await _httpClient.GetAsync("torrents/info", query);
|
||||||
|
|
||||||
@@ -379,6 +470,43 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return await GetJsonList<WebSeed>(response.Content);
|
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)
|
public async Task<IReadOnlyList<FileData>> GetTorrentContents(string hash, params int[] indexes)
|
||||||
{
|
{
|
||||||
var query = new QueryBuilder();
|
var query = new QueryBuilder();
|
||||||
@@ -411,18 +539,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
return await GetJsonList<string>(response.Content);
|
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)
|
public async Task StopTorrents(bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -433,18 +549,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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)
|
public async Task StartTorrents(bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -479,10 +583,11 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||||
|
.AddIfNotNullOrEmpty("urls", trackers is null ? null : string.Join('|', trackers))
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/reannounce", content);
|
var response = await _httpClient.PostAsync("torrents/reannounce", content);
|
||||||
@@ -490,13 +595,15 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
await ThrowIfNotSuccessfulStatusCode(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddTorrent(AddTorrentParams addTorrentParams)
|
public async Task<AddTorrentResult> AddTorrent(AddTorrentParams addTorrentParams)
|
||||||
{
|
{
|
||||||
var content = new MultipartFormDataContent();
|
var content = new MultipartFormDataContent();
|
||||||
if (addTorrentParams.Urls is not null)
|
|
||||||
|
if (addTorrentParams.Urls?.Any() == true)
|
||||||
{
|
{
|
||||||
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
|
content.AddString("urls", string.Join('\n', addTorrentParams.Urls));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addTorrentParams.Torrents is not null)
|
if (addTorrentParams.Torrents is not null)
|
||||||
{
|
{
|
||||||
foreach (var (name, stream) in addTorrentParams.Torrents)
|
foreach (var (name, stream) in addTorrentParams.Torrents)
|
||||||
@@ -504,6 +611,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
content.Add(new StreamContent(stream), "torrents", name);
|
content.Add(new StreamContent(stream), "torrents", name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (addTorrentParams.SkipChecking is not null)
|
if (addTorrentParams.SkipChecking is not null)
|
||||||
{
|
{
|
||||||
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
|
content.AddString("skip_checking", addTorrentParams.SkipChecking.Value);
|
||||||
@@ -520,12 +628,10 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
|
content.AddString("addToTopOfQueue", addTorrentParams.AddToTopOfQueue.Value);
|
||||||
}
|
}
|
||||||
// v4
|
if (addTorrentParams.Forced is not null)
|
||||||
if (addTorrentParams.Paused is not null)
|
|
||||||
{
|
{
|
||||||
content.AddString("paused", addTorrentParams.Paused.Value);
|
content.AddString("forced", addTorrentParams.Forced.Value);
|
||||||
}
|
}
|
||||||
// v5
|
|
||||||
if (addTorrentParams.Stopped is not null)
|
if (addTorrentParams.Stopped is not null)
|
||||||
{
|
{
|
||||||
content.AddString("stopped", addTorrentParams.Stopped.Value);
|
content.AddString("stopped", addTorrentParams.Stopped.Value);
|
||||||
@@ -590,21 +696,61 @@ namespace Lantean.QBitTorrentClient
|
|||||||
{
|
{
|
||||||
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
|
content.AddString("contentLayout", addTorrentParams.ContentLayout.Value);
|
||||||
}
|
}
|
||||||
|
if (addTorrentParams.Downloader is not null)
|
||||||
if (addTorrentParams.Cookie 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);
|
var response = await _httpClient.PostAsync("torrents/add", content);
|
||||||
|
|
||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
if (response.StatusCode == HttpStatusCode.Conflict)
|
||||||
|
{
|
||||||
|
var conflictMessage = await response.Content.ReadAsStringAsync();
|
||||||
|
if (string.IsNullOrWhiteSpace(conflictMessage))
|
||||||
|
{
|
||||||
|
conflictMessage = "All torrents failed to add.";
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task AddTrackersToTorrent(string hash, IEnumerable<string> urls)
|
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(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()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||||
.Add("urls", string.Join('\n', urls))
|
.Add("urls", string.Join('\n', urls))
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
@@ -613,23 +759,42 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.Add("hash", hash)
|
||||||
.Add("originalUrl", originalUrl)
|
.Add("url", url);
|
||||||
.Add("newUrl", newUrl)
|
|
||||||
.ToFormUrlEncodedContent();
|
|
||||||
|
|
||||||
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);
|
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()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.Add("hash", hash)
|
.AddAllOrPipeSeparated("hash", all, hashes ?? Array.Empty<string>())
|
||||||
.AddPipeSeparated("urls", urls)
|
.AddPipeSeparated("urls", urls)
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
@@ -732,13 +897,14 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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()
|
var content = new FormUrlEncodedBuilder()
|
||||||
.AddAllOrPipeSeparated("hashes", all, hashes)
|
.AddAllOrPipeSeparated("hashes", all, hashes)
|
||||||
.Add("ratioLimit", ratioLimit)
|
.Add("ratioLimit", ratioLimit)
|
||||||
.Add("seedingTimeLimit", seedingTimeLimit)
|
.Add("seedingTimeLimit", seedingTimeLimit)
|
||||||
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
|
.Add("inactiveSeedingTimeLimit", inactiveSeedingTimeLimit)
|
||||||
|
.AddIfNotNullOrEmpty("shareLimitAction", shareLimitAction)
|
||||||
.ToFormUrlEncodedContent();
|
.ToFormUrlEncodedContent();
|
||||||
|
|
||||||
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
|
var response = await _httpClient.PostAsync("torrents/setShareLimits", content);
|
||||||
@@ -795,6 +961,18 @@ namespace Lantean.QBitTorrentClient
|
|||||||
await ThrowIfNotSuccessfulStatusCode(response);
|
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)
|
public async Task SetTorrentCategory(string category, bool? all = null, params string[] hashes)
|
||||||
{
|
{
|
||||||
var content = new FormUrlEncodedBuilder()
|
var content = new FormUrlEncodedBuilder()
|
||||||
@@ -995,8 +1173,180 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return Task.FromResult($"{_httpClient.BaseAddress}torrents/export?hash={hash}");
|
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
|
#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
|
#region RSS
|
||||||
|
|
||||||
public async Task AddRssFolder(string path)
|
public async Task AddRssFolder(string path)
|
||||||
|
|||||||
@@ -1,24 +1,10 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
using System.Linq;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient
|
namespace Lantean.QBitTorrentClient
|
||||||
{
|
{
|
||||||
public static class ApiClientExtensions
|
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)
|
public static Task StopTorrent(this IApiClient apiClient, string hash)
|
||||||
{
|
{
|
||||||
return apiClient.StopTorrents(null, hash);
|
return apiClient.StopTorrents(null, hash);
|
||||||
@@ -34,21 +20,6 @@ namespace Lantean.QBitTorrentClient
|
|||||||
return apiClient.StopTorrents(true);
|
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)
|
public static Task StartTorrent(this IApiClient apiClient, string hash)
|
||||||
{
|
{
|
||||||
return apiClient.StartTorrents(null, hash);
|
return apiClient.StartTorrents(null, hash);
|
||||||
@@ -158,7 +129,7 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
public static Task ReannounceTorrent(this IApiClient apiClient, string hash)
|
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)
|
public static async Task<IEnumerable<string>> RemoveUnusedCategories(this IApiClient apiClient)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ namespace Lantean.QBitTorrentClient.Converters
|
|||||||
{
|
{
|
||||||
writer.WriteNumberValue(0);
|
writer.WriteNumberValue(0);
|
||||||
}
|
}
|
||||||
else if (value.IsDefaltFolder)
|
else if (value.IsDefaultFolder)
|
||||||
{
|
{
|
||||||
writer.WriteNumberValue(1);
|
writer.WriteNumberValue(1);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,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
|
namespace Lantean.QBitTorrentClient
|
||||||
{
|
{
|
||||||
@@ -28,6 +32,12 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task SetApplicationPreferences(UpdatePreferences preferences);
|
Task SetApplicationPreferences(UpdatePreferences preferences);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<ApplicationCookie>> GetApplicationCookies();
|
||||||
|
|
||||||
|
Task SetApplicationCookies(IEnumerable<ApplicationCookie> cookies);
|
||||||
|
|
||||||
|
Task<string> RotateApiKey();
|
||||||
|
|
||||||
Task<string> GetDefaultSavePath();
|
Task<string> GetDefaultSavePath();
|
||||||
|
|
||||||
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
|
Task<IReadOnlyList<NetworkInterface>> GetNetworkInterfaces();
|
||||||
@@ -36,6 +46,14 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
#endregion Application
|
#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
|
#region Log
|
||||||
|
|
||||||
Task<IReadOnlyList<Log>> GetLog(bool? normal = null, bool? info = null, bool? warning = null, bool? critical = null, int? lastKnownId = null);
|
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
|
#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);
|
Task<TorrentProperties> GetTorrentProperties(string hash);
|
||||||
|
|
||||||
@@ -82,16 +100,18 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task<IReadOnlyList<WebSeed>> GetTorrentWebSeeds(string hash);
|
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<FileData>> GetTorrentContents(string hash, params int[] indexes);
|
||||||
|
|
||||||
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
|
Task<IReadOnlyList<PieceState>> GetTorrentPieceStates(string hash);
|
||||||
|
|
||||||
Task<IReadOnlyList<string>> GetTorrentPieceHashes(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 StartTorrents(bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task StopTorrents(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 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);
|
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 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);
|
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 SetTorrentName(string name, string hash);
|
||||||
|
|
||||||
|
Task SetTorrentComment(IEnumerable<string> hashes, string comment);
|
||||||
|
|
||||||
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
|
Task SetTorrentCategory(string category, bool? all = null, params string[] hashes);
|
||||||
|
|
||||||
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
|
Task<IReadOnlyDictionary<string, Category>> GetAllCategories();
|
||||||
@@ -172,8 +194,26 @@ namespace Lantean.QBitTorrentClient
|
|||||||
|
|
||||||
Task<string> GetExportUrl(string hash);
|
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
|
#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
|
#region RSS
|
||||||
|
|
||||||
Task AddRssFolder(string path);
|
Task AddRssFolder(string path);
|
||||||
|
|||||||
@@ -12,9 +12,8 @@
|
|||||||
|
|
||||||
public bool? AddToTopOfQueue { get; set; }
|
public bool? AddToTopOfQueue { get; set; }
|
||||||
|
|
||||||
// v4
|
public bool? Forced { get; set; }
|
||||||
public bool? Paused { get; set; }
|
|
||||||
// v5
|
|
||||||
public bool? Stopped { get; set; }
|
public bool? Stopped { get; set; }
|
||||||
|
|
||||||
public string? SavePath { get; set; }
|
public string? SavePath { get; set; }
|
||||||
@@ -47,7 +46,15 @@
|
|||||||
|
|
||||||
public TorrentContentLayout? ContentLayout { get; set; }
|
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; }
|
public Dictionary<string, Stream>? Torrents { get; set; }
|
||||||
}
|
}
|
||||||
|
|||||||
30
Lantean.QBitTorrentClient/Models/AddTorrentResult.cs
Normal file
30
Lantean.QBitTorrentClient/Models/AddTorrentResult.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
33
Lantean.QBitTorrentClient/Models/ApplicationCookie.cs
Normal file
33
Lantean.QBitTorrentClient/Models/ApplicationCookie.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
IReadOnlyList<string>? tags,
|
IReadOnlyList<string>? tags,
|
||||||
IReadOnlyList<string>? tagsRemoved,
|
IReadOnlyList<string>? tagsRemoved,
|
||||||
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
|
IReadOnlyDictionary<string, IReadOnlyList<string>> trackers,
|
||||||
|
IReadOnlyList<string>? trackersRemoved,
|
||||||
ServerState? serverState)
|
ServerState? serverState)
|
||||||
{
|
{
|
||||||
ResponseId = responseId;
|
ResponseId = responseId;
|
||||||
@@ -26,6 +27,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Tags = tags;
|
Tags = tags;
|
||||||
TagsRemoved = tagsRemoved;
|
TagsRemoved = tagsRemoved;
|
||||||
Trackers = trackers;
|
Trackers = trackers;
|
||||||
|
TrackersRemoved = trackersRemoved;
|
||||||
ServerState = serverState;
|
ServerState = serverState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
string? flags,
|
string? flags,
|
||||||
string? flagsDescription,
|
string? flagsDescription,
|
||||||
string? iPAddress,
|
string? iPAddress,
|
||||||
|
string? i2pDestination,
|
||||||
string? clientId,
|
string? clientId,
|
||||||
int? port,
|
int? port,
|
||||||
float? progress,
|
float? progress,
|
||||||
@@ -33,6 +34,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Flags = flags;
|
Flags = flags;
|
||||||
FlagsDescription = flagsDescription;
|
FlagsDescription = flagsDescription;
|
||||||
IPAddress = iPAddress;
|
IPAddress = iPAddress;
|
||||||
|
I2pDestination = i2pDestination;
|
||||||
ClientId = clientId;
|
ClientId = clientId;
|
||||||
Port = port;
|
Port = port;
|
||||||
Progress = progress;
|
Progress = progress;
|
||||||
@@ -71,6 +73,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("ip")]
|
[JsonPropertyName("ip")]
|
||||||
public string? IPAddress { get; }
|
public string? IPAddress { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("i2p_dest")]
|
||||||
|
public string? I2pDestination { get; }
|
||||||
|
|
||||||
[JsonPropertyName("peer_id_client")]
|
[JsonPropertyName("peer_id_client")]
|
||||||
public string? ClientId { get; }
|
public string? ClientId { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonConstructor]
|
[JsonConstructor]
|
||||||
public Preferences(
|
public Preferences(
|
||||||
bool addToTopOfQueue,
|
bool addToTopOfQueue,
|
||||||
|
bool addStoppedEnabled,
|
||||||
string addTrackers,
|
string addTrackers,
|
||||||
bool addTrackersEnabled,
|
bool addTrackersEnabled,
|
||||||
int altDlLimit,
|
int altDlLimit,
|
||||||
@@ -14,6 +15,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
bool alternativeWebuiEnabled,
|
bool alternativeWebuiEnabled,
|
||||||
string alternativeWebuiPath,
|
string alternativeWebuiPath,
|
||||||
string announceIp,
|
string announceIp,
|
||||||
|
int announcePort,
|
||||||
bool announceToAllTiers,
|
bool announceToAllTiers,
|
||||||
bool announceToAllTrackers,
|
bool announceToAllTrackers,
|
||||||
bool anonymousMode,
|
bool anonymousMode,
|
||||||
@@ -85,6 +87,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int i2pPort,
|
int i2pPort,
|
||||||
bool idnSupportEnabled,
|
bool idnSupportEnabled,
|
||||||
bool incompleteFilesExt,
|
bool incompleteFilesExt,
|
||||||
|
bool useUnwantedFolder,
|
||||||
bool ipFilterEnabled,
|
bool ipFilterEnabled,
|
||||||
string ipFilterPath,
|
string ipFilterPath,
|
||||||
bool ipFilterTrackers,
|
bool ipFilterTrackers,
|
||||||
@@ -92,6 +95,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
bool limitTcpOverhead,
|
bool limitTcpOverhead,
|
||||||
bool limitUtpRate,
|
bool limitUtpRate,
|
||||||
int listenPort,
|
int listenPort,
|
||||||
|
bool sslEnabled,
|
||||||
|
int sslListenPort,
|
||||||
string locale,
|
string locale,
|
||||||
bool lsd,
|
bool lsd,
|
||||||
bool mailNotificationAuthEnabled,
|
bool mailNotificationAuthEnabled,
|
||||||
@@ -112,7 +117,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int maxConnecPerTorrent,
|
int maxConnecPerTorrent,
|
||||||
int maxInactiveSeedingTime,
|
int maxInactiveSeedingTime,
|
||||||
bool maxInactiveSeedingTimeEnabled,
|
bool maxInactiveSeedingTimeEnabled,
|
||||||
int maxRatio,
|
float maxRatio,
|
||||||
int maxRatioAct,
|
int maxRatioAct,
|
||||||
bool maxRatioEnabled,
|
bool maxRatioEnabled,
|
||||||
int maxSeedingTime,
|
int maxSeedingTime,
|
||||||
@@ -160,6 +165,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
string savePath,
|
string savePath,
|
||||||
bool savePathChangedTmmEnabled,
|
bool savePathChangedTmmEnabled,
|
||||||
int saveResumeDataInterval,
|
int saveResumeDataInterval,
|
||||||
|
int saveStatisticsInterval,
|
||||||
Dictionary<string, SaveLocation> scanDirs,
|
Dictionary<string, SaveLocation> scanDirs,
|
||||||
int scheduleFromHour,
|
int scheduleFromHour,
|
||||||
int scheduleFromMin,
|
int scheduleFromMin,
|
||||||
@@ -177,12 +183,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int socketReceiveBufferSize,
|
int socketReceiveBufferSize,
|
||||||
int socketSendBufferSize,
|
int socketSendBufferSize,
|
||||||
bool ssrfMitigation,
|
bool ssrfMitigation,
|
||||||
bool startPausedEnabled,
|
|
||||||
int stopTrackerTimeout,
|
int stopTrackerTimeout,
|
||||||
string tempPath,
|
string tempPath,
|
||||||
bool tempPathEnabled,
|
bool tempPathEnabled,
|
||||||
bool torrentChangedTmmEnabled,
|
bool torrentChangedTmmEnabled,
|
||||||
string torrentContentLayout,
|
string torrentContentLayout,
|
||||||
|
string torrentContentRemoveOption,
|
||||||
int torrentFileSizeLimit,
|
int torrentFileSizeLimit,
|
||||||
string torrentStopCondition,
|
string torrentStopCondition,
|
||||||
int upLimit,
|
int upLimit,
|
||||||
@@ -192,10 +198,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int upnpLeaseDuration,
|
int upnpLeaseDuration,
|
||||||
bool useCategoryPathsInManualMode,
|
bool useCategoryPathsInManualMode,
|
||||||
bool useHttps,
|
bool useHttps,
|
||||||
|
bool ignoreSslErrors,
|
||||||
bool useSubcategories,
|
bool useSubcategories,
|
||||||
int utpTcpMixedMode,
|
int utpTcpMixedMode,
|
||||||
bool validateHttpsTrackerCertificate,
|
bool validateHttpsTrackerCertificate,
|
||||||
string webUiAddress,
|
string webUiAddress,
|
||||||
|
string webUiApiKey,
|
||||||
int webUiBanDuration,
|
int webUiBanDuration,
|
||||||
bool webUiClickjackingProtectionEnabled,
|
bool webUiClickjackingProtectionEnabled,
|
||||||
bool webUiCsrfProtectionEnabled,
|
bool webUiCsrfProtectionEnabled,
|
||||||
@@ -217,6 +225,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
)
|
)
|
||||||
{
|
{
|
||||||
AddToTopOfQueue = addToTopOfQueue;
|
AddToTopOfQueue = addToTopOfQueue;
|
||||||
|
AddStoppedEnabled = addStoppedEnabled;
|
||||||
AddTrackers = addTrackers;
|
AddTrackers = addTrackers;
|
||||||
AddTrackersEnabled = addTrackersEnabled;
|
AddTrackersEnabled = addTrackersEnabled;
|
||||||
AltDlLimit = altDlLimit;
|
AltDlLimit = altDlLimit;
|
||||||
@@ -224,6 +233,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
AlternativeWebuiEnabled = alternativeWebuiEnabled;
|
AlternativeWebuiEnabled = alternativeWebuiEnabled;
|
||||||
AlternativeWebuiPath = alternativeWebuiPath;
|
AlternativeWebuiPath = alternativeWebuiPath;
|
||||||
AnnounceIp = announceIp;
|
AnnounceIp = announceIp;
|
||||||
|
AnnouncePort = announcePort;
|
||||||
AnnounceToAllTiers = announceToAllTiers;
|
AnnounceToAllTiers = announceToAllTiers;
|
||||||
AnnounceToAllTrackers = announceToAllTrackers;
|
AnnounceToAllTrackers = announceToAllTrackers;
|
||||||
AnonymousMode = anonymousMode;
|
AnonymousMode = anonymousMode;
|
||||||
@@ -295,6 +305,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
I2pPort = i2pPort;
|
I2pPort = i2pPort;
|
||||||
IdnSupportEnabled = idnSupportEnabled;
|
IdnSupportEnabled = idnSupportEnabled;
|
||||||
IncompleteFilesExt = incompleteFilesExt;
|
IncompleteFilesExt = incompleteFilesExt;
|
||||||
|
UseUnwantedFolder = useUnwantedFolder;
|
||||||
IpFilterEnabled = ipFilterEnabled;
|
IpFilterEnabled = ipFilterEnabled;
|
||||||
IpFilterPath = ipFilterPath;
|
IpFilterPath = ipFilterPath;
|
||||||
IpFilterTrackers = ipFilterTrackers;
|
IpFilterTrackers = ipFilterTrackers;
|
||||||
@@ -302,6 +313,8 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
LimitTcpOverhead = limitTcpOverhead;
|
LimitTcpOverhead = limitTcpOverhead;
|
||||||
LimitUtpRate = limitUtpRate;
|
LimitUtpRate = limitUtpRate;
|
||||||
ListenPort = listenPort;
|
ListenPort = listenPort;
|
||||||
|
SslEnabled = sslEnabled;
|
||||||
|
SslListenPort = sslListenPort;
|
||||||
Locale = locale;
|
Locale = locale;
|
||||||
Lsd = lsd;
|
Lsd = lsd;
|
||||||
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
|
MailNotificationAuthEnabled = mailNotificationAuthEnabled;
|
||||||
@@ -370,6 +383,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
SavePath = savePath;
|
SavePath = savePath;
|
||||||
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
|
SavePathChangedTmmEnabled = savePathChangedTmmEnabled;
|
||||||
SaveResumeDataInterval = saveResumeDataInterval;
|
SaveResumeDataInterval = saveResumeDataInterval;
|
||||||
|
SaveStatisticsInterval = saveStatisticsInterval;
|
||||||
ScanDirs = scanDirs;
|
ScanDirs = scanDirs;
|
||||||
ScheduleFromHour = scheduleFromHour;
|
ScheduleFromHour = scheduleFromHour;
|
||||||
ScheduleFromMin = scheduleFromMin;
|
ScheduleFromMin = scheduleFromMin;
|
||||||
@@ -387,12 +401,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
SocketReceiveBufferSize = socketReceiveBufferSize;
|
SocketReceiveBufferSize = socketReceiveBufferSize;
|
||||||
SocketSendBufferSize = socketSendBufferSize;
|
SocketSendBufferSize = socketSendBufferSize;
|
||||||
SsrfMitigation = ssrfMitigation;
|
SsrfMitigation = ssrfMitigation;
|
||||||
StartPausedEnabled = startPausedEnabled;
|
|
||||||
StopTrackerTimeout = stopTrackerTimeout;
|
StopTrackerTimeout = stopTrackerTimeout;
|
||||||
TempPath = tempPath;
|
TempPath = tempPath;
|
||||||
TempPathEnabled = tempPathEnabled;
|
TempPathEnabled = tempPathEnabled;
|
||||||
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
|
TorrentChangedTmmEnabled = torrentChangedTmmEnabled;
|
||||||
TorrentContentLayout = torrentContentLayout;
|
TorrentContentLayout = torrentContentLayout;
|
||||||
|
TorrentContentRemoveOption = torrentContentRemoveOption;
|
||||||
TorrentFileSizeLimit = torrentFileSizeLimit;
|
TorrentFileSizeLimit = torrentFileSizeLimit;
|
||||||
TorrentStopCondition = torrentStopCondition;
|
TorrentStopCondition = torrentStopCondition;
|
||||||
UpLimit = upLimit;
|
UpLimit = upLimit;
|
||||||
@@ -402,10 +416,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
UpnpLeaseDuration = upnpLeaseDuration;
|
UpnpLeaseDuration = upnpLeaseDuration;
|
||||||
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
|
UseCategoryPathsInManualMode = useCategoryPathsInManualMode;
|
||||||
UseHttps = useHttps;
|
UseHttps = useHttps;
|
||||||
|
IgnoreSslErrors = ignoreSslErrors;
|
||||||
UseSubcategories = useSubcategories;
|
UseSubcategories = useSubcategories;
|
||||||
UtpTcpMixedMode = utpTcpMixedMode;
|
UtpTcpMixedMode = utpTcpMixedMode;
|
||||||
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
|
ValidateHttpsTrackerCertificate = validateHttpsTrackerCertificate;
|
||||||
WebUiAddress = webUiAddress;
|
WebUiAddress = webUiAddress;
|
||||||
|
WebUiApiKey = webUiApiKey;
|
||||||
WebUiBanDuration = webUiBanDuration;
|
WebUiBanDuration = webUiBanDuration;
|
||||||
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
|
WebUiClickjackingProtectionEnabled = webUiClickjackingProtectionEnabled;
|
||||||
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
|
WebUiCsrfProtectionEnabled = webUiCsrfProtectionEnabled;
|
||||||
@@ -429,6 +445,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("add_to_top_of_queue")]
|
[JsonPropertyName("add_to_top_of_queue")]
|
||||||
public bool AddToTopOfQueue { get; }
|
public bool AddToTopOfQueue { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("add_stopped_enabled")]
|
||||||
|
public bool AddStoppedEnabled { get; }
|
||||||
|
|
||||||
[JsonPropertyName("add_trackers")]
|
[JsonPropertyName("add_trackers")]
|
||||||
public string AddTrackers { get; }
|
public string AddTrackers { get; }
|
||||||
|
|
||||||
@@ -450,6 +469,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("announce_ip")]
|
[JsonPropertyName("announce_ip")]
|
||||||
public string AnnounceIp { get; }
|
public string AnnounceIp { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("announce_port")]
|
||||||
|
public int AnnouncePort { get; }
|
||||||
|
|
||||||
[JsonPropertyName("announce_to_all_tiers")]
|
[JsonPropertyName("announce_to_all_tiers")]
|
||||||
public bool AnnounceToAllTiers { get; }
|
public bool AnnounceToAllTiers { get; }
|
||||||
|
|
||||||
@@ -663,6 +685,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("incomplete_files_ext")]
|
[JsonPropertyName("incomplete_files_ext")]
|
||||||
public bool IncompleteFilesExt { get; }
|
public bool IncompleteFilesExt { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("use_unwanted_folder")]
|
||||||
|
public bool UseUnwantedFolder { get; }
|
||||||
|
|
||||||
[JsonPropertyName("ip_filter_enabled")]
|
[JsonPropertyName("ip_filter_enabled")]
|
||||||
public bool IpFilterEnabled { get; }
|
public bool IpFilterEnabled { get; }
|
||||||
|
|
||||||
@@ -684,6 +709,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("listen_port")]
|
[JsonPropertyName("listen_port")]
|
||||||
public int ListenPort { get; }
|
public int ListenPort { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_enabled")]
|
||||||
|
public bool SslEnabled { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_listen_port")]
|
||||||
|
public int SslListenPort { get; }
|
||||||
|
|
||||||
[JsonPropertyName("locale")]
|
[JsonPropertyName("locale")]
|
||||||
public string Locale { get; }
|
public string Locale { get; }
|
||||||
|
|
||||||
@@ -745,7 +776,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
public bool MaxInactiveSeedingTimeEnabled { get; }
|
public bool MaxInactiveSeedingTimeEnabled { get; }
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio")]
|
[JsonPropertyName("max_ratio")]
|
||||||
public int MaxRatio { get; }
|
public float MaxRatio { get; }
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio_act")]
|
[JsonPropertyName("max_ratio_act")]
|
||||||
public int MaxRatioAct { get; }
|
public int MaxRatioAct { get; }
|
||||||
@@ -888,6 +919,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("save_resume_data_interval")]
|
[JsonPropertyName("save_resume_data_interval")]
|
||||||
public int SaveResumeDataInterval { get; }
|
public int SaveResumeDataInterval { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("save_statistics_interval")]
|
||||||
|
public int SaveStatisticsInterval { get; }
|
||||||
|
|
||||||
[JsonPropertyName("scan_dirs")]
|
[JsonPropertyName("scan_dirs")]
|
||||||
public Dictionary<string, SaveLocation> ScanDirs { get; }
|
public Dictionary<string, SaveLocation> ScanDirs { get; }
|
||||||
|
|
||||||
@@ -939,9 +973,6 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("ssrf_mitigation")]
|
[JsonPropertyName("ssrf_mitigation")]
|
||||||
public bool SsrfMitigation { get; }
|
public bool SsrfMitigation { get; }
|
||||||
|
|
||||||
[JsonPropertyName("start_paused_enabled")]
|
|
||||||
public bool StartPausedEnabled { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("stop_tracker_timeout")]
|
[JsonPropertyName("stop_tracker_timeout")]
|
||||||
public int StopTrackerTimeout { get; }
|
public int StopTrackerTimeout { get; }
|
||||||
|
|
||||||
@@ -957,6 +988,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("torrent_content_layout")]
|
[JsonPropertyName("torrent_content_layout")]
|
||||||
public string TorrentContentLayout { get; }
|
public string TorrentContentLayout { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("torrent_content_remove_option")]
|
||||||
|
public string TorrentContentRemoveOption { get; }
|
||||||
|
|
||||||
[JsonPropertyName("torrent_file_size_limit")]
|
[JsonPropertyName("torrent_file_size_limit")]
|
||||||
public int TorrentFileSizeLimit { get; }
|
public int TorrentFileSizeLimit { get; }
|
||||||
|
|
||||||
@@ -984,6 +1018,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("use_https")]
|
[JsonPropertyName("use_https")]
|
||||||
public bool UseHttps { get; }
|
public bool UseHttps { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ignore_ssl_errors")]
|
||||||
|
public bool IgnoreSslErrors { get; }
|
||||||
|
|
||||||
[JsonPropertyName("use_subcategories")]
|
[JsonPropertyName("use_subcategories")]
|
||||||
public bool UseSubcategories { get; }
|
public bool UseSubcategories { get; }
|
||||||
|
|
||||||
@@ -996,6 +1033,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("web_ui_address")]
|
[JsonPropertyName("web_ui_address")]
|
||||||
public string WebUiAddress { get; }
|
public string WebUiAddress { get; }
|
||||||
|
|
||||||
|
[JsonPropertyName("web_ui_api_key")]
|
||||||
|
public string WebUiApiKey { get; }
|
||||||
|
|
||||||
[JsonPropertyName("web_ui_ban_duration")]
|
[JsonPropertyName("web_ui_ban_duration")]
|
||||||
public int WebUiBanDuration { get; }
|
public int WebUiBanDuration { get; }
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
{
|
{
|
||||||
public bool IsWatchedFolder { get; set; }
|
public bool IsWatchedFolder { get; set; }
|
||||||
|
|
||||||
public bool IsDefaltFolder { get; set; }
|
public bool IsDefaultFolder { get; set; }
|
||||||
|
|
||||||
public string? SavePath { get; set; }
|
public string? SavePath { get; set; }
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
IsDefaltFolder = true
|
IsDefaultFolder = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,7 +40,7 @@
|
|||||||
{
|
{
|
||||||
return new SaveLocation
|
return new SaveLocation
|
||||||
{
|
{
|
||||||
IsDefaltFolder = true
|
IsDefaultFolder = true
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
else if (IsDefaltFolder)
|
else if (IsDefaultFolder)
|
||||||
{
|
{
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,264 +1,219 @@
|
|||||||
using Lantean.QBitTorrentClient.Converters;
|
using Lantean.QBitTorrentClient.Converters;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.Json.Serialization;
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
public record Torrent
|
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")]
|
[JsonPropertyName("hash")]
|
||||||
public string Hash { get; }
|
public string Hash { get; init; } = string.Empty;
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v1")]
|
[JsonPropertyName("infohash_v1")]
|
||||||
public string? InfoHashV1 { get; }
|
public string? InfoHashV1 { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v2")]
|
[JsonPropertyName("infohash_v2")]
|
||||||
public string? InfoHashV2 { get; }
|
public string? InfoHashV2 { get; init; }
|
||||||
|
|
||||||
[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; }
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
[JsonPropertyName("name")]
|
||||||
public string? Name { get; }
|
public string? Name { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("num_complete")]
|
[JsonPropertyName("magnet_uri")]
|
||||||
public int? NumberComplete { get; }
|
public string? MagnetUri { get; init; }
|
||||||
|
|
||||||
[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("size")]
|
[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")]
|
[JsonPropertyName("state")]
|
||||||
public string? State { get; }
|
public string? State { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("super_seeding")]
|
[JsonPropertyName("seq_dl")]
|
||||||
public bool? SuperSeeding { get; }
|
public bool? SequentialDownload { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("f_l_piece_prio")]
|
||||||
|
public bool? FirstLastPiecePriority { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("category")]
|
||||||
|
public string? Category { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
[JsonPropertyName("tags")]
|
||||||
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
||||||
public IReadOnlyList<string>? Tags { get; }
|
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
||||||
|
|
||||||
[JsonPropertyName("time_active")]
|
[JsonPropertyName("super_seeding")]
|
||||||
public int? TimeActive { get; }
|
public bool? SuperSeeding { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("total_size")]
|
[JsonPropertyName("force_start")]
|
||||||
public long? TotalSize { get; }
|
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")]
|
[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")]
|
[JsonPropertyName("up_limit")]
|
||||||
public long? UploadLimit { get; }
|
public long? UploadLimit { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("downloaded")]
|
||||||
|
public long? Downloaded { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("uploaded")]
|
[JsonPropertyName("uploaded")]
|
||||||
public long? Uploaded { get; }
|
public long? Uploaded { get; init; }
|
||||||
|
|
||||||
|
[JsonPropertyName("downloaded_session")]
|
||||||
|
public long? DownloadedSession { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("uploaded_session")]
|
[JsonPropertyName("uploaded_session")]
|
||||||
public long? UploadedSession { get; }
|
public long? UploadedSession { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("upspeed")]
|
[JsonPropertyName("amount_left")]
|
||||||
public long? UploadSpeed { get; }
|
public long? AmountLeft { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("reannounce")]
|
[JsonPropertyName("completed")]
|
||||||
public long? Reannounce { get; }
|
public long? Completed { get; init; }
|
||||||
|
|
||||||
[JsonPropertyName("inactive_seeding_time_limit")]
|
[JsonPropertyName("connections_count")]
|
||||||
public float? InactiveSeedingTimeLimit { get; }
|
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")]
|
[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; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
131
Lantean.QBitTorrentClient/Models/TorrentCreationTask.cs
Normal file
131
Lantean.QBitTorrentClient/Models/TorrentCreationTask.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
105
Lantean.QBitTorrentClient/Models/TorrentMetadata.cs
Normal file
105
Lantean.QBitTorrentClient/Models/TorrentMetadata.cs
Normal 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);
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
long downloadLimit,
|
long downloadLimit,
|
||||||
long downloadSpeed,
|
long downloadSpeed,
|
||||||
long downloadSpeedAverage,
|
long downloadSpeedAverage,
|
||||||
int estimatedTimeOfArrival,
|
long estimatedTimeOfArrival,
|
||||||
long lastSeen,
|
long lastSeen,
|
||||||
int connections,
|
int connections,
|
||||||
int connectionsLimit,
|
int connectionsLimit,
|
||||||
@@ -104,7 +104,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
public long DownloadSpeedAverage { get; }
|
public long DownloadSpeedAverage { get; }
|
||||||
|
|
||||||
[JsonPropertyName("eta")]
|
[JsonPropertyName("eta")]
|
||||||
public int EstimatedTimeOfArrival { get; }
|
public long EstimatedTimeOfArrival { get; }
|
||||||
|
|
||||||
[JsonPropertyName("last_seen")]
|
[JsonPropertyName("last_seen")]
|
||||||
public long LastSeen { get; }
|
public long LastSeen { get; }
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
@@ -13,7 +15,10 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
int seeds,
|
int seeds,
|
||||||
int leeches,
|
int leeches,
|
||||||
int downloads,
|
int downloads,
|
||||||
string message)
|
string message,
|
||||||
|
long? nextAnnounce,
|
||||||
|
long? minAnnounce,
|
||||||
|
IReadOnlyList<TrackerEndpoint>? endpoints)
|
||||||
{
|
{
|
||||||
Url = url;
|
Url = url;
|
||||||
Status = status;
|
Status = status;
|
||||||
@@ -23,6 +28,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
Leeches = leeches;
|
Leeches = leeches;
|
||||||
Downloads = downloads;
|
Downloads = downloads;
|
||||||
Message = message;
|
Message = message;
|
||||||
|
NextAnnounce = nextAnnounce;
|
||||||
|
MinAnnounce = minAnnounce;
|
||||||
|
Endpoints = endpoints ?? Array.Empty<TrackerEndpoint>();
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName("url")]
|
[JsonPropertyName("url")]
|
||||||
@@ -48,5 +56,27 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("msg")]
|
[JsonPropertyName("msg")]
|
||||||
public string Message { get; }
|
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);
|
||||||
}
|
}
|
||||||
@@ -7,5 +7,7 @@
|
|||||||
Working = 2,
|
Working = 2,
|
||||||
Updating = 3,
|
Updating = 3,
|
||||||
NotWorking = 4,
|
NotWorking = 4,
|
||||||
|
Error = 5,
|
||||||
|
Unreachable = 6
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Text.Json.Serialization;
|
using System;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
namespace Lantean.QBitTorrentClient.Models
|
||||||
{
|
{
|
||||||
@@ -7,6 +8,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("add_to_top_of_queue")]
|
[JsonPropertyName("add_to_top_of_queue")]
|
||||||
public bool? AddToTopOfQueue { get; set; }
|
public bool? AddToTopOfQueue { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("add_stopped_enabled")]
|
||||||
|
public bool? AddStoppedEnabled { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("add_trackers")]
|
[JsonPropertyName("add_trackers")]
|
||||||
public string? AddTrackers { get; set; }
|
public string? AddTrackers { get; set; }
|
||||||
|
|
||||||
@@ -28,6 +32,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("announce_ip")]
|
[JsonPropertyName("announce_ip")]
|
||||||
public string? AnnounceIp { get; set; }
|
public string? AnnounceIp { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("announce_port")]
|
||||||
|
public int? AnnouncePort { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("announce_to_all_tiers")]
|
[JsonPropertyName("announce_to_all_tiers")]
|
||||||
public bool? AnnounceToAllTiers { get; set; }
|
public bool? AnnounceToAllTiers { get; set; }
|
||||||
|
|
||||||
@@ -241,6 +248,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("incomplete_files_ext")]
|
[JsonPropertyName("incomplete_files_ext")]
|
||||||
public bool? IncompleteFilesExt { get; set; }
|
public bool? IncompleteFilesExt { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("use_unwanted_folder")]
|
||||||
|
public bool? UseUnwantedFolder { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("ip_filter_enabled")]
|
[JsonPropertyName("ip_filter_enabled")]
|
||||||
public bool? IpFilterEnabled { get; set; }
|
public bool? IpFilterEnabled { get; set; }
|
||||||
|
|
||||||
@@ -262,6 +272,12 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("listen_port")]
|
[JsonPropertyName("listen_port")]
|
||||||
public int? ListenPort { get; set; }
|
public int? ListenPort { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_enabled")]
|
||||||
|
public bool? SslEnabled { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ssl_listen_port")]
|
||||||
|
public int? SslListenPort { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("locale")]
|
[JsonPropertyName("locale")]
|
||||||
public string? Locale { get; set; }
|
public string? Locale { get; set; }
|
||||||
|
|
||||||
@@ -323,7 +339,7 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
public bool? MaxInactiveSeedingTimeEnabled { get; set; }
|
public bool? MaxInactiveSeedingTimeEnabled { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio")]
|
[JsonPropertyName("max_ratio")]
|
||||||
public int? MaxRatio { get; set; }
|
public float? MaxRatio { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio_act")]
|
[JsonPropertyName("max_ratio_act")]
|
||||||
public int? MaxRatioAct { get; set; }
|
public int? MaxRatioAct { get; set; }
|
||||||
@@ -466,6 +482,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("save_resume_data_interval")]
|
[JsonPropertyName("save_resume_data_interval")]
|
||||||
public int? SaveResumeDataInterval { get; set; }
|
public int? SaveResumeDataInterval { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("save_statistics_interval")]
|
||||||
|
public int? SaveStatisticsInterval { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("scan_dirs")]
|
[JsonPropertyName("scan_dirs")]
|
||||||
public Dictionary<string, SaveLocation>? ScanDirs { get; set; }
|
public Dictionary<string, SaveLocation>? ScanDirs { get; set; }
|
||||||
|
|
||||||
@@ -517,9 +536,6 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("ssrf_mitigation")]
|
[JsonPropertyName("ssrf_mitigation")]
|
||||||
public bool? SsrfMitigation { get; set; }
|
public bool? SsrfMitigation { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("start_paused_enabled")]
|
|
||||||
public bool? StartPausedEnabled { get; set; }
|
|
||||||
|
|
||||||
[JsonPropertyName("stop_tracker_timeout")]
|
[JsonPropertyName("stop_tracker_timeout")]
|
||||||
public int? StopTrackerTimeout { get; set; }
|
public int? StopTrackerTimeout { get; set; }
|
||||||
|
|
||||||
@@ -535,6 +551,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("torrent_content_layout")]
|
[JsonPropertyName("torrent_content_layout")]
|
||||||
public string? TorrentContentLayout { get; set; }
|
public string? TorrentContentLayout { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("torrent_content_remove_option")]
|
||||||
|
public string? TorrentContentRemoveOption { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("torrent_file_size_limit")]
|
[JsonPropertyName("torrent_file_size_limit")]
|
||||||
public int? TorrentFileSizeLimit { get; set; }
|
public int? TorrentFileSizeLimit { get; set; }
|
||||||
|
|
||||||
@@ -562,6 +581,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("use_https")]
|
[JsonPropertyName("use_https")]
|
||||||
public bool? UseHttps { get; set; }
|
public bool? UseHttps { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("ignore_ssl_errors")]
|
||||||
|
public bool? IgnoreSslErrors { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("use_subcategories")]
|
[JsonPropertyName("use_subcategories")]
|
||||||
public bool? UseSubcategories { get; set; }
|
public bool? UseSubcategories { get; set; }
|
||||||
|
|
||||||
@@ -574,6 +596,9 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
[JsonPropertyName("web_ui_address")]
|
[JsonPropertyName("web_ui_address")]
|
||||||
public string? WebUiAddress { get; set; }
|
public string? WebUiAddress { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("web_ui_api_key")]
|
||||||
|
public string? WebUiApiKey { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("web_ui_ban_duration")]
|
[JsonPropertyName("web_ui_ban_duration")]
|
||||||
public int? WebUiBanDuration { get; set; }
|
public int? WebUiBanDuration { get; set; }
|
||||||
|
|
||||||
@@ -627,5 +652,23 @@ namespace Lantean.QBitTorrentClient.Models
|
|||||||
|
|
||||||
[JsonPropertyName("web_ui_password")]
|
[JsonPropertyName("web_ui_password")]
|
||||||
public string? WebUiPassword { get; set; }
|
public string? WebUiPassword { get; set; }
|
||||||
|
|
||||||
|
public void Validate()
|
||||||
|
{
|
||||||
|
if (MaxRatio.HasValue && MaxRatioEnabled.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Specify either max_ratio or max_ratio_enabled, not both.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaxSeedingTime.HasValue && MaxSeedingTimeEnabled.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Specify either max_seeding_time or max_seeding_time_enabled, not both.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (MaxInactiveSeedingTime.HasValue && MaxInactiveSeedingTimeEnabled.HasValue)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Specify either max_inactive_seeding_time or max_inactive_seeding_time_enabled, not both.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
5
global.json
Normal file
5
global.json
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"sdk": {
|
||||||
|
"version": "9.0.306"
|
||||||
|
}
|
||||||
|
}
|
||||||
94
readme.md
94
readme.md
@@ -1,14 +1,84 @@
|
|||||||
# qbt-mud
|
# qbtmud
|
||||||
|
|
||||||
## To-Do
|
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
|
||||||
|
|
||||||
- Rename multiple files dialog
|
## Features
|
||||||
- ~~RSS feeds and dialogs~~
|
|
||||||
- ~~About~~
|
qbtmud replicates all core features of the qBittorrent WebUI, including:
|
||||||
- ~~Context menu for files list/trackers list/peers list~~
|
|
||||||
- ~~Tag management page~~
|
- **Torrent Management** – Add, remove, and control torrents.
|
||||||
- ~~Category management page~~
|
- **Tracker Control** – View and manage trackers.
|
||||||
- ~~Update all tables to use DynamicTable~~
|
- **Peer Management** – Monitor and manage peers connected to torrents.
|
||||||
- ~~Log~~
|
- **File Prioritization** – Select and prioritize specific files within a torrent.
|
||||||
- ~~Blocks~~
|
- **Speed Limits** – Set global and per-torrent speed limits.
|
||||||
- ~~Search~~
|
- **RSS Integration** – Subscribe to RSS feeds for automated torrent downloads.
|
||||||
|
- **Search Functionality** – Integrated torrent search.
|
||||||
|
- **Sequential Downloading** – Download files in order for media streaming.
|
||||||
|
- **Super Seeding Mode** – Efficiently distribute torrents as an initial seeder.
|
||||||
|
- **IP Filtering** – Improve security by filtering specific IP addresses.
|
||||||
|
- **IPv6 Support** – Full support for IPv6 networks.
|
||||||
|
- **Bandwidth Scheduler** – Schedule bandwidth limits.
|
||||||
|
- **WebUI Access** – Remotely manage torrents through the WebUI.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
For a detailed explanation of these features, refer to the [qBittorrent Options Guide](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install qbtmud without building from source:
|
||||||
|
|
||||||
|
### 1. Download the Latest Release
|
||||||
|
- Go to the [qbtmud Releases](https://github.com/lantean-code/qbtmud/releases) page.
|
||||||
|
- Download the latest release archive for your operating system.
|
||||||
|
|
||||||
|
### 2. Extract the Archive
|
||||||
|
- Extract the contents of the downloaded archive to a directory of your choice.
|
||||||
|
|
||||||
|
### 3. Configure qBittorrent to Use qbtmud
|
||||||
|
- Open qBittorrent and navigate to `Tools` > `Options` > `Web UI`.
|
||||||
|
- Enable the option **"Use alternative WebUI"**.
|
||||||
|
- Set the **"Root Folder"** to the directory where you extracted qbtmud.
|
||||||
|
- Click **OK** to save the settings.
|
||||||
|
|
||||||
|
### 4. Access qbtmud
|
||||||
|
- Open your web browser and go to `http://localhost:8080` (or the port configured in qBittorrent).
|
||||||
|
|
||||||
|
For more detailed instructions, refer to the [Alternate WebUI Usage Guide](https://github.com/qbittorrent/qBittorrent/wiki/Alternate-WebUI-usage).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
To build qbtmud from source, you need to have the **.NET 9.0 SDK** installed on your system.
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/lantean-code/qbtmud.git
|
||||||
|
cd qbtmud
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restore Dependencies
|
||||||
|
```sh
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build and Publish the Application
|
||||||
|
```sh
|
||||||
|
dotnet publish --configuration Release
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output the Web UI files to `Lantean.QBTMud\bin\Release\net9.0\publish\wwwroot`.
|
||||||
|
|
||||||
|
### 4. Configure qBittorrent to Use qbtmud
|
||||||
|
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
|
||||||
|
|
||||||
|
### 5. Run qbtmud
|
||||||
|
Navigate to the directory containing the built files and run the application using the appropriate command for your OS.
|
||||||
|
|
||||||
|
By following these steps, you can set up qbtmud to manage your qBittorrent server with an improved web interface, offering better functionality and usability.
|
||||||
|
|||||||
Reference in New Issue
Block a user