mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-28 02:23:52 +00:00
Compare commits
21 Commits
1.2.0
...
feature/v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4666cb0b36 | ||
|
|
498420bf23 | ||
|
|
9d2ba7c57b | ||
|
|
2e843b1191 | ||
|
|
eb13b83548 | ||
|
|
e9daca6568 | ||
|
|
075ea9f855 | ||
|
|
d01204a703 | ||
|
|
ab1c594b07 | ||
|
|
6a5d8b2610 | ||
|
|
b8412bb232 | ||
|
|
e64a13c7c9 | ||
|
|
e4ea79a8ed | ||
|
|
0976b72411 | ||
|
|
965fbcd010 | ||
|
|
3d0dbde9f4 | ||
|
|
5b4fbde7b2 | ||
|
|
0db0ad4374 | ||
|
|
c390d83e4d | ||
|
|
8dd29c238d | ||
|
|
fca17edfd1 |
60
AGENTS.md
Normal file
60
AGENTS.md
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# AGENTS.md (root)
|
||||||
|
|
||||||
|
> Scope: This file gives high-level context and guardrails for the repository root. Deeper folders may add their own `AGENTS.md` files which take precedence for their subtrees (e.g., `./src/AGENTS.md`, `./test/AGENTS.md`).
|
||||||
|
|
||||||
|
## Project overview
|
||||||
|
- qbtmud is a drop-in replacement for qBittorrent's default WebUI, aiming for full feature parity with a modern UI.
|
||||||
|
- Primary goals: parity with the default WebUI, excellent UX, reliability, and easy installation.
|
||||||
|
- Non-goals: diverging from qBittorrent semantics without explicit design approval.
|
||||||
|
|
||||||
|
## Repository layout
|
||||||
|
- Solution: `Lantean.QBTMud.sln`
|
||||||
|
- Projects:
|
||||||
|
- `Lantean.QBTMud` — Web UI host and published assets.
|
||||||
|
- `Lantean.QBitTorrentClient` — client library for qBittorrent Web API.
|
||||||
|
- `Lantean.QBTMud.Test` — unit tests.
|
||||||
|
- `Lantean.QBitTorrentClient.Test` — unit tests.
|
||||||
|
- Config/conventions: `.editorconfig`, `.gitattributes`, `nuget.config`, `global.json` (SDK pin).
|
||||||
|
|
||||||
|
## Build, test, publish
|
||||||
|
- Prerequisites: .NET 9 SDK (use the version pinned by `global.json` if present).
|
||||||
|
- Restore & build:
|
||||||
|
- `dotnet restore`
|
||||||
|
- `dotnet build --configuration Release`
|
||||||
|
- Run tests:
|
||||||
|
- `dotnet test`
|
||||||
|
- Publish Web UI:
|
||||||
|
- `dotnet publish Lantean.QBTMud -c Release`
|
||||||
|
- Output (static assets): `Lantean.QBTMud/bin/Release/net9.0/publish/wwwroot/`
|
||||||
|
|
||||||
|
## Coding and test standards
|
||||||
|
- Source code rules and generation constraints live in `./src/AGENTS.md` (authoritative for code style, design, docs).
|
||||||
|
- Unit test rules live in `./test/AGENTS.md` (authoritative for test structure, naming, mocks, coverage).
|
||||||
|
- If rules conflict, the deeper file (closer to the change) wins; otherwise, follow both.
|
||||||
|
|
||||||
|
## How to work in this repo (for agents)
|
||||||
|
1. Read this file, then the relevant folder `AGENTS.md` (e.g., `src` or `test`).
|
||||||
|
2. Before modifying code:
|
||||||
|
- Confirm SDK target, nullable context, analyzers, and editorconfig rules.
|
||||||
|
- Keep public surface consistent; do not break qBittorrent Web API expectations without approval.
|
||||||
|
3. When generating code:
|
||||||
|
- Follow `./src/AGENTS.md` exactly (naming, formatting, docs, DI, async, security).
|
||||||
|
- Prefer minimal, maintainable changes; avoid churn to unrelated files.
|
||||||
|
4. When writing tests:
|
||||||
|
- Follow `./test/AGENTS.md` exactly (class/method naming, `_target`, mocks, coverage).
|
||||||
|
5. Before opening a PR:
|
||||||
|
- Build succeeds, tests are green.
|
||||||
|
- Public XML docs added/updated.
|
||||||
|
- Changelog notes in the PR description (what changed, why, risks, testing).
|
||||||
|
|
||||||
|
## PR and review checklist
|
||||||
|
- [ ] Change is scoped and well-justified; no unrelated edits.
|
||||||
|
- [ ] Code adheres to `./src/AGENTS.md` standards.
|
||||||
|
- [ ] Tests adhere to `./test/AGENTS.md` and achieve required coverage.
|
||||||
|
- [ ] No secrets, tokens, or user-specific paths committed.
|
||||||
|
- [ ] Builds with the pinned SDK; `dotnet restore`, `build`, `test`, and `publish` succeed.
|
||||||
|
- [ ] Error messages and logs are clear and actionable.
|
||||||
|
|
||||||
|
## Communication & assumptions
|
||||||
|
- Do not guess. If any requirement, API contract, or behavior is unclear, ask for clarification.
|
||||||
|
- Prefer concise diffs and explicit rationale in commit messages and PR descriptions.
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
|
|
||||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.8.34511.84
|
VisualStudioVersion = 18.0.11121.172 d18.0
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud.Test", "Lantean.QBTMud.Test\Lantean.QBTMud.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud.Test", "test\Lantean.QBTMud.Test\Lantean.QBTMud.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "src\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud", "Lantean.QBTMud\Lantean.QBTMud.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud", "src\Lantean.QBTMud\Lantean.QBTMud.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
@@ -15,6 +15,10 @@ 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", "test\Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{344EAF42-5D2B-4F56-8B28-1F3158A37E0A}"
|
||||||
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
Debug|Any CPU = Debug|Any CPU
|
||||||
@@ -33,10 +37,18 @@ 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
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{715E075C-1D86-4A7F-BC72-E1E24A294F17} = {344EAF42-5D2B-4F56-8B28-1F3158A37E0A}
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75} = {344EAF42-5D2B-4F56-8B28-1F3158A37E0A}
|
||||||
|
EndGlobalSection
|
||||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
|
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
|||||||
@@ -1,68 +0,0 @@
|
|||||||
<MudGrid>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
<MudCollapse Expanded="Expanded">
|
|
||||||
<MudGrid>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
|
||||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
@if (ShowCookieOption)
|
|
||||||
{
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
|
|
||||||
@foreach (var category in Categories)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="category">@category</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Start torrent" @bind-Value="StartTorrent" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("None")">None</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
|
||||||
</MudItem>
|
|
||||||
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudCollapse>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
|
||||||
using Lantean.QBTMud.Models;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.Dialogs
|
|
||||||
{
|
|
||||||
public partial class AddTorrentOptions
|
|
||||||
{
|
|
||||||
[Inject]
|
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public bool ShowCookieOption { get; set; }
|
|
||||||
|
|
||||||
protected bool Expanded { get; set; }
|
|
||||||
|
|
||||||
protected bool TorrentManagementMode { get; set; }
|
|
||||||
|
|
||||||
protected string SavePath { get; set; } = default!;
|
|
||||||
|
|
||||||
protected string? Cookie { get; set; }
|
|
||||||
|
|
||||||
protected string? RenameTorrent { get; set; }
|
|
||||||
|
|
||||||
protected IEnumerable<string> Categories { get; set; } = [];
|
|
||||||
|
|
||||||
protected string? Category { get; set; }
|
|
||||||
|
|
||||||
protected bool StartTorrent { get; set; } = true;
|
|
||||||
|
|
||||||
protected bool AddToTopOfQueue { get; set; } = true;
|
|
||||||
|
|
||||||
protected string StopCondition { get; set; } = "None";
|
|
||||||
|
|
||||||
protected bool SkipHashCheck { get; set; } = false;
|
|
||||||
|
|
||||||
protected string ContentLayout { get; set; } = "Original";
|
|
||||||
|
|
||||||
protected bool DownloadInSequentialOrder { get; set; } = false;
|
|
||||||
|
|
||||||
protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false;
|
|
||||||
|
|
||||||
protected long DownloadLimit { get; set; }
|
|
||||||
|
|
||||||
protected long UploadLimit { get; set; }
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var categories = await ApiClient.GetAllCategories();
|
|
||||||
Categories = categories.Select(c => c.Key).ToList();
|
|
||||||
|
|
||||||
var preferences = await ApiClient.GetApplicationPreferences();
|
|
||||||
|
|
||||||
TorrentManagementMode = preferences.AutoTmmEnabled;
|
|
||||||
SavePath = preferences.SavePath;
|
|
||||||
StartTorrent = !preferences.StartPausedEnabled;
|
|
||||||
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
|
||||||
StopCondition = preferences.TorrentStopCondition;
|
|
||||||
ContentLayout = preferences.TorrentContentLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TorrentOptions GetTorrentOptions()
|
|
||||||
{
|
|
||||||
return new TorrentOptions(
|
|
||||||
TorrentManagementMode,
|
|
||||||
SavePath,
|
|
||||||
Cookie,
|
|
||||||
RenameTorrent,
|
|
||||||
Category,
|
|
||||||
StartTorrent,
|
|
||||||
AddToTopOfQueue,
|
|
||||||
StopCondition,
|
|
||||||
SkipHashCheck,
|
|
||||||
ContentLayout,
|
|
||||||
DownloadInSequentialOrder,
|
|
||||||
DownloadFirstAndLastPiecesFirst,
|
|
||||||
DownloadLimit,
|
|
||||||
UploadLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
namespace Lantean.QBTMud.Helpers
|
|
||||||
{
|
|
||||||
internal static class VersionHelper
|
|
||||||
{
|
|
||||||
private static int? _version;
|
|
||||||
|
|
||||||
private const int _defaultVersion = 5;
|
|
||||||
|
|
||||||
public static int DefaultVersion => _defaultVersion;
|
|
||||||
|
|
||||||
public static int GetMajorVersion(string? version)
|
|
||||||
{
|
|
||||||
if (_version is not null)
|
|
||||||
{
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (string.IsNullOrEmpty(version))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
|
|
||||||
{
|
|
||||||
return _defaultVersion;
|
|
||||||
}
|
|
||||||
|
|
||||||
_version = theVersion.Major;
|
|
||||||
|
|
||||||
return _version.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net9.0</TargetFramework>
|
|
||||||
<Nullable>enable</Nullable>
|
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
|
||||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
|
||||||
<CompressionEnabled>false</CompressionEnabled>
|
|
||||||
<LangVersion>12</LangVersion>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
|
||||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
|
|
||||||
<PackageReference Include="MudBlazor" Version="8.13.0" />
|
|
||||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
</Project>
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
@inherits LayoutComponentBase
|
|
||||||
|
|
||||||
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
|
|
||||||
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
|
|
||||||
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
|
|
||||||
<MudDialogProvider CloseOnEscapeKey="true" />
|
|
||||||
<MudSnackbarProvider />
|
|
||||||
<MudPopoverProvider />
|
|
||||||
|
|
||||||
<PageTitle>qBittorrent Web UI</PageTitle>
|
|
||||||
|
|
||||||
<MudLayout>
|
|
||||||
<MudAppBar>
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
|
|
||||||
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
|
|
||||||
<MudSpacer />
|
|
||||||
@if (ErrorBoundary?.Errors.Count > 0)
|
|
||||||
{
|
|
||||||
<MudBadge Content="@(ErrorBoundary?.Errors.Count ?? 0)" Color="Color.Error" Overlap="true" Class="mr-2">
|
|
||||||
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
|
|
||||||
</MudBadge>
|
|
||||||
}
|
|
||||||
<MudSwitch T="bool" Label="Dark Mode" LabelPlacement="Placement.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
|
|
||||||
<Menu @ref="Menu" />
|
|
||||||
</MudAppBar>
|
|
||||||
<MudDrawer @bind-Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
|
|
||||||
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
|
|
||||||
</MudDrawer>
|
|
||||||
<CascadingValue Value="Theme">
|
|
||||||
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
|
|
||||||
<CascadingValue Value="Menu">
|
|
||||||
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
|
|
||||||
@Body
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</MudLayout>
|
|
||||||
</EnhancedErrorBoundary>
|
|
||||||
</CascadingValue>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
namespace Lantean.QBTMud.Models
|
|
||||||
{
|
|
||||||
public readonly struct FilterState
|
|
||||||
{
|
|
||||||
public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms)
|
|
||||||
{
|
|
||||||
Category = category;
|
|
||||||
Status = status;
|
|
||||||
Tag = tag;
|
|
||||||
Tracker = tracker;
|
|
||||||
UseSubcategories = useSubcategories;
|
|
||||||
Terms = terms;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Category { get; } = "all";
|
|
||||||
public Status Status { get; } = Status.All;
|
|
||||||
public string Tag { get; } = "all";
|
|
||||||
public string Tracker { get; } = "all";
|
|
||||||
public bool UseSubcategories { get; }
|
|
||||||
public string? Terms { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
namespace Lantean.QBTMud.Models
|
|
||||||
{
|
|
||||||
public class SearchForm
|
|
||||||
{
|
|
||||||
public string? SearchText { get; set; }
|
|
||||||
|
|
||||||
public string SelectedPlugin { get; set; } = "all";
|
|
||||||
|
|
||||||
public string SelectedCategory { get; set; } = "all";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
@page "/search"
|
|
||||||
@layout OtherLayout
|
|
||||||
|
|
||||||
<div class="content-panel">
|
|
||||||
<div class="content-panel__toolbar">
|
|
||||||
<MudToolBar Gutters="false" Dense="true">
|
|
||||||
@if (!DrawerOpen)
|
|
||||||
{
|
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
}
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
<MudText Class="pl-5 no-wrap">Search</MudText>
|
|
||||||
</MudToolBar>
|
|
||||||
</div>
|
|
||||||
<div class="content-panel__body">
|
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
|
||||||
<MudCardContent>
|
|
||||||
<EditForm Model="Model" OnValidSubmit="DoSearch">
|
|
||||||
<MudGrid>
|
|
||||||
<MudItem xs="12" md="4">
|
|
||||||
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="3">
|
|
||||||
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
|
|
||||||
@foreach (var (value, name) in Categories)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="value">@name</MudSelectItem>
|
|
||||||
if (value == "all")
|
|
||||||
{
|
|
||||||
<MudDivider />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="3">
|
|
||||||
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("all")">All</MudSelectItem>
|
|
||||||
@if (Plugins.Count > 0)
|
|
||||||
{
|
|
||||||
<MudDivider />
|
|
||||||
|
|
||||||
}
|
|
||||||
@foreach (var (value, name) in Plugins)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="value">@name</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12" md="2">
|
|
||||||
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
|
|
||||||
</MudItem>
|
|
||||||
|
|
||||||
</MudGrid>
|
|
||||||
</EditForm>
|
|
||||||
</MudCardContent>
|
|
||||||
</MudCard>
|
|
||||||
|
|
||||||
<DynamicTable @ref="Table"
|
|
||||||
T="Lantean.QBitTorrentClient.Models.SearchResult"
|
|
||||||
ColumnDefinitions="Columns"
|
|
||||||
Items="Results"
|
|
||||||
MultiSelection="false"
|
|
||||||
SelectOnRowClick="false"
|
|
||||||
Class="search-list content-panel__table" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
|
||||||
using Lantean.QBTMud.Components.UI;
|
|
||||||
using Lantean.QBTMud.Helpers;
|
|
||||||
using Lantean.QBTMud.Models;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
|
||||||
using MudBlazor;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Pages
|
|
||||||
{
|
|
||||||
public partial class Search : IDisposable
|
|
||||||
{
|
|
||||||
private IReadOnlyList<QBitTorrentClient.Models.SearchPlugin>? _plugins;
|
|
||||||
private int? _searchId;
|
|
||||||
private bool _disposedValue;
|
|
||||||
private readonly CancellationTokenSource _timerCancellationToken = new();
|
|
||||||
private readonly int _refreshInterval = 1500;
|
|
||||||
|
|
||||||
private QBitTorrentClient.Models.SearchResults? _searchResults;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected IDialogService DialogService { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
|
||||||
|
|
||||||
[CascadingParameter]
|
|
||||||
public MainData? MainData { get; set; }
|
|
||||||
|
|
||||||
[CascadingParameter(Name = "DrawerOpen")]
|
|
||||||
public bool DrawerOpen { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public string? Hash { get; set; }
|
|
||||||
|
|
||||||
protected SearchForm Model { get; set; } = new SearchForm();
|
|
||||||
|
|
||||||
protected Dictionary<string, string> Plugins => _plugins is null ? [] : _plugins.ToDictionary(a => a.Name, a => a.FullName);
|
|
||||||
|
|
||||||
protected Dictionary<string, string> Categories => GetCategories(Model.SelectedPlugin);
|
|
||||||
|
|
||||||
protected IEnumerable<QBitTorrentClient.Models.SearchResult>? Results => _searchResults?.Results;
|
|
||||||
|
|
||||||
protected DynamicTable<QBitTorrentClient.Models.SearchResult>? Table { get; set; }
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
_plugins = await ApiClient.GetSearchPlugins();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_refreshInterval)))
|
|
||||||
{
|
|
||||||
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
|
||||||
{
|
|
||||||
if (_searchId is not null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_searchResults = await ApiClient.GetSearchResults(_searchId.Value);
|
|
||||||
|
|
||||||
if (_searchResults.Status == "Stopped")
|
|
||||||
{
|
|
||||||
await ApiClient.DeleteSearch(_searchId.Value);
|
|
||||||
_searchId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException)
|
|
||||||
{
|
|
||||||
if (MainData is not null)
|
|
||||||
{
|
|
||||||
MainData.LostConnection = true;
|
|
||||||
}
|
|
||||||
_searchId = null;
|
|
||||||
}
|
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void NavigateBack()
|
|
||||||
{
|
|
||||||
NavigationManager.NavigateTo("/");
|
|
||||||
}
|
|
||||||
|
|
||||||
private Dictionary<string, string> GetCategories(string plugin)
|
|
||||||
{
|
|
||||||
if (_plugins is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (plugin == "all")
|
|
||||||
{
|
|
||||||
return _plugins.SelectMany(i => i.SupportedCategories).Distinct().ToDictionary(a => a.Id, a => a.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
var pluginItem = _plugins.FirstOrDefault(p => p.Name == plugin);
|
|
||||||
if (pluginItem is null)
|
|
||||||
{
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
return pluginItem.SupportedCategories.ToDictionary(a => a.Id, a => a.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task DoSearch(EditContext editContext)
|
|
||||||
{
|
|
||||||
if (_searchId is null)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrEmpty(Model.SearchText))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_searchResults = null;
|
|
||||||
_searchId = await ApiClient.StartSearch(Model.SearchText, [Model.SelectedPlugin], Model.SelectedCategory);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var status = await ApiClient.GetSearchStatus(_searchId.Value);
|
|
||||||
|
|
||||||
if (status is not null)
|
|
||||||
{
|
|
||||||
if (status.Status == "Running")
|
|
||||||
{
|
|
||||||
await ApiClient.StopSearch(_searchId.Value);
|
|
||||||
}
|
|
||||||
|
|
||||||
await ApiClient.DeleteSearch(_searchId.Value);
|
|
||||||
|
|
||||||
_searchId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (HttpRequestException exception) when (exception.StatusCode == System.Net.HttpStatusCode.NotFound)
|
|
||||||
{
|
|
||||||
_searchId = null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected IEnumerable<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> Columns => ColumnsDefinitions;
|
|
||||||
|
|
||||||
public static List<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> ColumnsDefinitions { get; } =
|
|
||||||
[
|
|
||||||
new ColumnDefinition<QBitTorrentClient.Models.SearchResult>("Name", l => l.FileName),
|
|
||||||
new ColumnDefinition<QBitTorrentClient.Models.SearchResult>("Size", l => @DisplayHelpers.Size(l.FileSize)),
|
|
||||||
new ColumnDefinition<QBitTorrentClient.Models.SearchResult>("Seeders", l => l.Seeders),
|
|
||||||
new ColumnDefinition<QBitTorrentClient.Models.SearchResult>("Leechers", l => l.Leechers),
|
|
||||||
new ColumnDefinition<QBitTorrentClient.Models.SearchResult>("Search engine", l => l.SiteUrl),
|
|
||||||
];
|
|
||||||
|
|
||||||
protected virtual void Dispose(bool disposing)
|
|
||||||
{
|
|
||||||
if (!_disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
_timerCancellationToken.Cancel();
|
|
||||||
_timerCancellationToken.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
|
||||||
Dispose(disposing: true);
|
|
||||||
GC.SuppressFinalize(this);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
namespace Lantean.QBitTorrentClient.Models
|
|
||||||
{
|
|
||||||
public class SaveLocation
|
|
||||||
{
|
|
||||||
public bool IsWatchedFolder { get; set; }
|
|
||||||
|
|
||||||
public bool IsDefaultFolder { get; set; }
|
|
||||||
|
|
||||||
public string? SavePath { get; set; }
|
|
||||||
|
|
||||||
public static SaveLocation Create(object? value)
|
|
||||||
{
|
|
||||||
if (value is int intValue)
|
|
||||||
{
|
|
||||||
if (intValue == 0)
|
|
||||||
{
|
|
||||||
return new SaveLocation
|
|
||||||
{
|
|
||||||
IsWatchedFolder = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (intValue == 1)
|
|
||||||
{
|
|
||||||
return new SaveLocation
|
|
||||||
{
|
|
||||||
IsDefaultFolder = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (value is string stringValue)
|
|
||||||
{
|
|
||||||
if (stringValue == "0")
|
|
||||||
{
|
|
||||||
return new SaveLocation
|
|
||||||
{
|
|
||||||
IsWatchedFolder = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else if (stringValue == "1")
|
|
||||||
{
|
|
||||||
return new SaveLocation
|
|
||||||
{
|
|
||||||
IsDefaultFolder = true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
return new SaveLocation
|
|
||||||
{
|
|
||||||
SavePath = stringValue
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(value));
|
|
||||||
}
|
|
||||||
|
|
||||||
public object ToValue()
|
|
||||||
{
|
|
||||||
if (IsWatchedFolder)
|
|
||||||
{
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
else if (IsDefaultFolder)
|
|
||||||
{
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
else if (SavePath is not null)
|
|
||||||
{
|
|
||||||
return SavePath;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new InvalidOperationException("Invalid value.");
|
|
||||||
}
|
|
||||||
|
|
||||||
public override string? ToString()
|
|
||||||
{
|
|
||||||
return ToValue().ToString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
using Lantean.QBitTorrentClient.Converters;
|
|
||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
|
||||||
{
|
|
||||||
public record Torrent
|
|
||||||
{
|
|
||||||
[JsonConstructor]
|
|
||||||
public Torrent(
|
|
||||||
long? addedOn,
|
|
||||||
long? amountLeft,
|
|
||||||
bool? automaticTorrentManagement,
|
|
||||||
float? availability,
|
|
||||||
string? category,
|
|
||||||
long? completed,
|
|
||||||
long? completionOn,
|
|
||||||
string? contentPath,
|
|
||||||
long? downloadLimit,
|
|
||||||
long? downloadSpeed,
|
|
||||||
long? downloaded,
|
|
||||||
long? downloadedSession,
|
|
||||||
long? estimatedTimeOfArrival,
|
|
||||||
bool? firstLastPiecePriority,
|
|
||||||
bool? forceStart,
|
|
||||||
string hash,
|
|
||||||
string? infoHashV1,
|
|
||||||
string? infoHashV2,
|
|
||||||
long? lastActivity,
|
|
||||||
string? magnetUri,
|
|
||||||
float? maxRatio,
|
|
||||||
int? maxSeedingTime,
|
|
||||||
string? name,
|
|
||||||
int? numberComplete,
|
|
||||||
int? numberIncomplete,
|
|
||||||
int? numberLeeches,
|
|
||||||
int? numberSeeds,
|
|
||||||
int? priority,
|
|
||||||
float? progress,
|
|
||||||
float? ratio,
|
|
||||||
float? ratioLimit,
|
|
||||||
string? savePath,
|
|
||||||
long? seedingTime,
|
|
||||||
int? seedingTimeLimit,
|
|
||||||
long? seenComplete,
|
|
||||||
bool? sequentialDownload,
|
|
||||||
long? size,
|
|
||||||
string? state,
|
|
||||||
bool? superSeeding,
|
|
||||||
IReadOnlyList<string>? tags,
|
|
||||||
int? timeActive,
|
|
||||||
long? totalSize,
|
|
||||||
string? tracker,
|
|
||||||
long? uploadLimit,
|
|
||||||
long? uploaded,
|
|
||||||
long? uploadedSession,
|
|
||||||
long? uploadSpeed,
|
|
||||||
long? reannounce,
|
|
||||||
float? inactiveSeedingTimeLimit,
|
|
||||||
float? maxInactiveSeedingTime)
|
|
||||||
{
|
|
||||||
AddedOn = addedOn;
|
|
||||||
AmountLeft = amountLeft;
|
|
||||||
AutomaticTorrentManagement = automaticTorrentManagement;
|
|
||||||
Availability = availability;
|
|
||||||
Category = category;
|
|
||||||
Completed = completed;
|
|
||||||
CompletionOn = completionOn;
|
|
||||||
ContentPath = contentPath;
|
|
||||||
DownloadLimit = downloadLimit;
|
|
||||||
DownloadSpeed = downloadSpeed;
|
|
||||||
Downloaded = downloaded;
|
|
||||||
DownloadedSession = downloadedSession;
|
|
||||||
EstimatedTimeOfArrival = estimatedTimeOfArrival;
|
|
||||||
FirstLastPiecePriority = firstLastPiecePriority;
|
|
||||||
ForceStart = forceStart;
|
|
||||||
Hash = hash;
|
|
||||||
InfoHashV1 = infoHashV1;
|
|
||||||
InfoHashV2 = infoHashV2;
|
|
||||||
LastActivity = lastActivity;
|
|
||||||
MagnetUri = magnetUri;
|
|
||||||
MaxRatio = maxRatio;
|
|
||||||
MaxSeedingTime = maxSeedingTime;
|
|
||||||
Name = name;
|
|
||||||
NumberComplete = numberComplete;
|
|
||||||
NumberIncomplete = numberIncomplete;
|
|
||||||
NumberLeeches = numberLeeches;
|
|
||||||
NumberSeeds = numberSeeds;
|
|
||||||
Priority = priority;
|
|
||||||
Progress = progress;
|
|
||||||
Ratio = ratio;
|
|
||||||
RatioLimit = ratioLimit;
|
|
||||||
SavePath = savePath;
|
|
||||||
SeedingTime = seedingTime;
|
|
||||||
SeedingTimeLimit = seedingTimeLimit;
|
|
||||||
SeenComplete = seenComplete;
|
|
||||||
SequentialDownload = sequentialDownload;
|
|
||||||
Size = size;
|
|
||||||
State = state;
|
|
||||||
SuperSeeding = superSeeding;
|
|
||||||
Tags = tags ?? [];
|
|
||||||
TimeActive = timeActive;
|
|
||||||
TotalSize = totalSize;
|
|
||||||
Tracker = tracker;
|
|
||||||
UploadLimit = uploadLimit;
|
|
||||||
Uploaded = uploaded;
|
|
||||||
UploadedSession = uploadedSession;
|
|
||||||
UploadSpeed = uploadSpeed;
|
|
||||||
Reannounce = reannounce;
|
|
||||||
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
|
|
||||||
MaxInactiveSeedingTime = maxInactiveSeedingTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("added_on")]
|
|
||||||
public long? AddedOn { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("amount_left")]
|
|
||||||
public long? AmountLeft { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("auto_tmm")]
|
|
||||||
public bool? AutomaticTorrentManagement { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("availability")]
|
|
||||||
public float? Availability { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("category")]
|
|
||||||
public string? Category { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("completed")]
|
|
||||||
public long? Completed { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("completion_on")]
|
|
||||||
public long? CompletionOn { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("content_path")]
|
|
||||||
public string? ContentPath { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("dl_limit")]
|
|
||||||
public long? DownloadLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("dlspeed")]
|
|
||||||
public long? DownloadSpeed { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("downloaded")]
|
|
||||||
public long? Downloaded { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("downloaded_session")]
|
|
||||||
public long? DownloadedSession { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("eta")]
|
|
||||||
public long? EstimatedTimeOfArrival { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("f_l_piece_prio")]
|
|
||||||
public bool? FirstLastPiecePriority { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("force_start")]
|
|
||||||
public bool? ForceStart { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("hash")]
|
|
||||||
public string Hash { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v1")]
|
|
||||||
public string? InfoHashV1 { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("infohash_v2")]
|
|
||||||
public string? InfoHashV2 { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("last_activity")]
|
|
||||||
public long? LastActivity { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("magnet_uri")]
|
|
||||||
public string? MagnetUri { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_ratio")]
|
|
||||||
public float? MaxRatio { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_seeding_time")]
|
|
||||||
public int? MaxSeedingTime { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("name")]
|
|
||||||
public string? Name { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_complete")]
|
|
||||||
public int? NumberComplete { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_incomplete")]
|
|
||||||
public int? NumberIncomplete { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_leechs")]
|
|
||||||
public int? NumberLeeches { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_seeds")]
|
|
||||||
public int? NumberSeeds { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("priority")]
|
|
||||||
public int? Priority { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("progress")]
|
|
||||||
public float? Progress { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("ratio")]
|
|
||||||
public float? Ratio { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("ratio_limit")]
|
|
||||||
public float? RatioLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("save_path")]
|
|
||||||
public string? SavePath { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seeding_time")]
|
|
||||||
public long? SeedingTime { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seeding_time_limit")]
|
|
||||||
public int? SeedingTimeLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seen_complete")]
|
|
||||||
public long? SeenComplete { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("seq_dl")]
|
|
||||||
public bool? SequentialDownload { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("size")]
|
|
||||||
public long? Size { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("state")]
|
|
||||||
public string? State { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("super_seeding")]
|
|
||||||
public bool? SuperSeeding { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("tags")]
|
|
||||||
[JsonConverter(typeof(CommaSeparatedJsonConverter))]
|
|
||||||
public IReadOnlyList<string>? Tags { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("time_active")]
|
|
||||||
public int? TimeActive { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("total_size")]
|
|
||||||
public long? TotalSize { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("tracker")]
|
|
||||||
public string? Tracker { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("up_limit")]
|
|
||||||
public long? UploadLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("uploaded")]
|
|
||||||
public long? Uploaded { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("uploaded_session")]
|
|
||||||
public long? UploadedSession { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("upspeed")]
|
|
||||||
public long? UploadSpeed { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("reannounce")]
|
|
||||||
public long? Reannounce { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("inactive_seeding_time_limit")]
|
|
||||||
public float? InactiveSeedingTimeLimit { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("max_inactive_seeding_time")]
|
|
||||||
public float? MaxInactiveSeedingTime { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
using System.Text.Json.Serialization;
|
|
||||||
|
|
||||||
namespace Lantean.QBitTorrentClient.Models
|
|
||||||
{
|
|
||||||
public record TorrentTracker
|
|
||||||
{
|
|
||||||
[JsonConstructor]
|
|
||||||
public TorrentTracker(
|
|
||||||
string url,
|
|
||||||
TrackerStatus status,
|
|
||||||
int tier,
|
|
||||||
int peers,
|
|
||||||
int seeds,
|
|
||||||
int leeches,
|
|
||||||
int downloads,
|
|
||||||
string message)
|
|
||||||
{
|
|
||||||
Url = url;
|
|
||||||
Status = status;
|
|
||||||
Tier = tier;
|
|
||||||
Peers = peers;
|
|
||||||
Seeds = seeds;
|
|
||||||
Leeches = leeches;
|
|
||||||
Downloads = downloads;
|
|
||||||
Message = message;
|
|
||||||
}
|
|
||||||
|
|
||||||
[JsonPropertyName("url")]
|
|
||||||
public string Url { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("status")]
|
|
||||||
public TrackerStatus Status { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("tier")]
|
|
||||||
public int Tier { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_peers")]
|
|
||||||
public int Peers { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_seeds")]
|
|
||||||
public int Seeds { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_leeches")]
|
|
||||||
public int Leeches { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("num_downloaded")]
|
|
||||||
public int Downloads { get; }
|
|
||||||
|
|
||||||
[JsonPropertyName("msg")]
|
|
||||||
public string Message { get; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
77
Search-Implementation.md
Normal file
77
Search-Implementation.md
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
# Search Feature Parity Plan
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
- Bring the `qbt-mud` search experience to functional parity with the qBittorrent v5 WebUI, including multi-job handling, advanced result filtering, and plugin management.
|
||||||
|
- Reuse existing infrastructure (MudBlazor tables, dialogs, API client) wherever possible while filling the missing pieces.
|
||||||
|
- Deliver the work in incremental, testable slices that keep existing search behaviour stable until parity is achieved.
|
||||||
|
|
||||||
|
## Current Implementation Snapshot
|
||||||
|
- `Lantean.QBTMud/Pages/Search.razor` renders a simple form that runs a single search against one plugin and displays results in a static `DynamicTable` without row actions.
|
||||||
|
- `Lantean.QBTMud/Pages/Search.razor.cs` only stores the last `_searchId`, polls `search/results` until the job stops, and discards previous jobs.
|
||||||
|
- `SearchResult` models lack `EngineName`/`PublishedOn` data and the UI never exposes description/download links.
|
||||||
|
- There is no UI for plugin enable/disable/install/update, nor support for saved search filters (min seeds/size, search-in scope).
|
||||||
|
- The API client already exposes all `search/*` endpoints, but the UI consumes only a subset.
|
||||||
|
|
||||||
|
## Gap Analysis vs qBittorrent v5
|
||||||
|
- **Search job lifecycle**: v5 keeps a tab per job, shows statuses from `search/status`, supports restarting/completing jobs, and allows switching between them. `qbt-mud` supports only one ephemeral job.
|
||||||
|
- **Form inputs & filters**: v5 enables searching across *enabled* plugins, selecting multiple specific plugins, and provides client-side filters (search-in scope, seeds range, size range). Current form offers only single-plugin + category.
|
||||||
|
- **Results table**: v5 streams batched results (limit/offset), displays engine/site/pub date, exposes context menu actions (download, open description, copy data), and shows visible/total result counts. Current table is read-only with limited columns.
|
||||||
|
- **Search plugin management**: v5 surfaces enabled state, version, URL, manual install (local/URL), uninstall, enable/disable toggles, and update-all. There is no corresponding UI in `qbt-mud`.
|
||||||
|
- **State persistence**: v5 stores granular filter preferences locally. `qbt-mud` has no saved state for search filters or column selection specific to search.
|
||||||
|
- **Accessibility/UX**: parity requires toolbar buttons (stop/refresh/close job), empty-state messaging (no plugins/searches), and error surfaces when API calls fail.
|
||||||
|
|
||||||
|
## Implementation Plan
|
||||||
|
|
||||||
|
### 1. Search Job Lifecycle & State Management
|
||||||
|
- Introduce a dedicated view-model (e.g., `SearchJobViewModel`) to track pattern, selected plugins, category, status, totals, timestamps, and accumulated results. Locate in `Lantean.QBTMud/Models`.
|
||||||
|
- Expand `Search.razor.cs` to maintain a collection of jobs keyed by id, using `GetSearchesStatus()` for periodic synchronization and `GetSearchResults(id, limit, offset)` to stream additional rows.
|
||||||
|
- Replace the single `_searchId`/`_searchResults` fields with job-centric state, and schedule polling via `PeriodicTimer` per active job. Ensure timers dispose cleanly on navigation/dispose.
|
||||||
|
- Update the razor markup to display job tabs or a side list (matching MudBlazor’s `MudTabs` or `MudList`) allowing context menu actions (refresh, close, close all). Mirror qBittorrent behaviour where “Stop” cancels a running job and “Search” starts a new one without clearing previous jobs.
|
||||||
|
- Reflect job status icons/text (Running, Stopped, Aborted, Error) and total result counts in the UI; surface API failures through existing toast/dialog mechanisms.
|
||||||
|
- Files impacted: `Lantean.QBTMud/Pages/Search.razor`, `Lantean.QBTMud/Pages/Search.razor.cs`, `Lantean.QBTMud/Models/SearchForm.cs`, new `Lantean.QBTMud/Models/SearchJobViewModel.cs`.
|
||||||
|
|
||||||
|
### 2. Search Form & Filters
|
||||||
|
- Extend `SearchForm` with multi-select plugin selection (`ICollection<string> SelectedPlugins`), a special “Enabled plugins” option, search-in scope, and optional min/max seeds & size filters (with units). Persist defaults via `ILocalStorageService`.
|
||||||
|
- Update the form markup to use `MudSelect` with `MultiSelection="true"` and chips for selected plugins, add numeric inputs for seeds/size with validation, and wire a text filter box for client-side result filtering.
|
||||||
|
- Implement client-side filtering in `Search.razor.cs` by applying the configured filters to each job’s accumulated results before binding them to `DynamicTable` (similar to qBittorrent’s `search.js` behaviour). Consider extracting a helper (`SearchFilterHelper`) for readability.
|
||||||
|
- Include UI affordances for empty states (no plugins installed, no searches yet) and a “Manage plugins…” button that opens the plugin dialog.
|
||||||
|
- Files impacted: `Lantean.QBTMud/Pages/Search.razor`, `Lantean.QBTMud/Pages/Search.razor.cs`, `Lantean.QBTMud/Models/SearchForm.cs`, new helper under `Lantean.QBTMud/Helpers/SearchFilterHelper.cs`.
|
||||||
|
|
||||||
|
### 3. Search Results Table & Row Actions
|
||||||
|
- Expand the column definitions to match v5 (`Name`, `Size`, `Seeders`, `Leechers`, `Engine`, `Site`, `Published`, optional `Actions`). Update `ColumnsDefinitions` in `Search.razor.cs` and ensure `DynamicTable` can render links/buttons inside rows.
|
||||||
|
- Add a row-action menu leveraging `DynamicTable`’s `OnTableDataContextMenu` to provide “Download”, “Open description”, “Copy → Name/Download link/Description URL” options. Implement the handlers in the code-behind, reusing `DialogHelper.InvokeAddTorrentLinkDialog` and clipboard utilities.
|
||||||
|
- Track and display the visible vs total result counts per job (using `SearchResults.Total` + post-filter counts) and surface in the UI header.
|
||||||
|
- Support incremental result loading by requesting in batches (e.g., 200-500 items) with offset; append to the job’s result list and trigger table refresh without re-fetching the full dataset.
|
||||||
|
- Files impacted: `Lantean.QBTMud/Pages/Search.razor`, `Lantean.QBTMud/Pages/Search.razor.cs`, possibly `Lantean.QBTMud/Components/UI/DynamicTable.razor.cs` (if new hooks required), clipboard utilities in `Lantean.QBTMud/Helpers`.
|
||||||
|
|
||||||
|
### 4. Search Plugin Management Experience
|
||||||
|
- Create a dialog (e.g., `SearchPluginsDialog.razor` + `.razor.cs`) presenting the plugin list with columns for enabled, name, version, URL, and last update. Include actions to enable/disable (batch), uninstall, install from URL/path, and update all.
|
||||||
|
- Wire the dialog into the search page “Manage plugins…” button and optionally from settings. Ensure optimistic UI updates after each command with error fallback.
|
||||||
|
- Provide basic validation for install sources (URL/local path) and progress feedback (loading spinner, success/fail toasts).
|
||||||
|
- Files impacted: new component under `Lantean.QBTMud/Components/Dialogs/SearchPluginsDialog.*`, updates to `Lantean.QBTMud/Helpers/DialogHelper.cs` (shortcut methods), and `Lantean.QBTMud/Pages/Search.razor` for invocation.
|
||||||
|
|
||||||
|
### 5. Client & Model Updates
|
||||||
|
- Update `Lantean.QBitTorrentClient/Models/SearchResult.cs` to include `EngineName`, `SiteUrl` (already), and `PublishedOn` (`pubDate`) properties with appropriate JSON bindings. Adjust constructors and equality semantics accordingly.
|
||||||
|
- Audit `Lantean.QBTMud` consumers for the new properties and update them to display `EngineName` instead of reusing `SiteUrl` for plugin name.
|
||||||
|
- Validate whether `SearchStatus` needs extra fields (e.g., `Plugin`) in v5 API responses; extend the model if necessary and adapt `ApiClientSearchTests` fixtures.
|
||||||
|
- Ensure `StartSearch` can accept “enabled” and multi-plugin input. Update `DoSearch` to send either `["enabled"]` or the selected plugin names without wrapping them in an array when empty. Handle cases where no plugin is selected gracefully.
|
||||||
|
- Files impacted: `Lantean.QBitTorrentClient/Models/SearchResult.cs`, `Lantean.QBitTorrentClient/Models/SearchStatus.cs` (if needed), `Lantean.QBitTorrentClient/ApiClient.cs`, `Lantean.QBitTorrentClient.Test/ApiClientSearchTests.cs`, downstream mapping code in `Lantean.QBTMud`.
|
||||||
|
|
||||||
|
### 6. Testing & Validation
|
||||||
|
- Unit tests: extend `ApiClientSearchTests` to cover the new `SearchResult` fields and multi-plugin payload logic. Add tests for the filter helper to ensure parity with v5 behaviour (min/max seeds/size, pattern matching, search-in scope).
|
||||||
|
- Component/integration tests: create bUnit tests for the search page covering (a) job creation and stop flow, (b) filtering behaviour, and (c) context menu actions invoking expected API calls or helper methods.
|
||||||
|
- Manual QA checklist: verify multi-job tabs, plugin install/uninstall flows, incremental result loading, download actions, and resilience to API failures (404, timeouts). Include mobile viewport sanity checks for responsive layout.
|
||||||
|
|
||||||
|
## Assumptions & Open Questions
|
||||||
|
- qBittorrent v5 continues to expose `engineName` and `pubDate` fields; confirm with a sample response before implementing.
|
||||||
|
- Determine whether search results should persist across sessions (v5 clears on reload); initial plan assumes in-memory only.
|
||||||
|
- Confirm availability of clipboard services within existing helper infrastructure or add a consistent abstraction.
|
||||||
|
- Check whether MudBlazor can render high-density tab headers akin to v5; if not, consider a vertical `MudNavMenu` for job selection.
|
||||||
|
|
||||||
|
## Suggested Sequencing
|
||||||
|
- Stage 1: Model/client updates + unit tests (ensures data shapes are correct).
|
||||||
|
- Stage 2: Search page refactor to multi-job architecture (retain basic table).
|
||||||
|
- Stage 3: Layer in advanced filters and result actions.
|
||||||
|
- Stage 4: Add plugin management dialog and wiring.
|
||||||
|
- Stage 5: Polish UX (counts, empty states, toasts) and execute full QA pass.
|
||||||
|
|
||||||
99
Unit-Testing-Plan.md
Normal file
99
Unit-Testing-Plan.md
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
# bUnit Coverage Expansion Plan
|
||||||
|
|
||||||
|
## Objectives
|
||||||
|
- Establish a modern component testing stack for `Lantean.QBTMud` using bUnit + xUnit so critical UI flows can be validated without manual regression.
|
||||||
|
- Provide structured guidance for converting the existing placeholder tests into meaningful component coverage, prioritising high-value pages and shared UI primitives.
|
||||||
|
- Ensure the plan dovetails with the broader qBittorrent v5 alignment work (e.g., new search experience, torrent actions, dialogs).
|
||||||
|
|
||||||
|
## Current Test Landscape
|
||||||
|
- `Lantean.QBTMud.Test` is already configured as an xUnit project but contains only experimental/unit scaffolding (`UnitTest1.cs`). No component tests run today.
|
||||||
|
- No bUnit, MudBlazor test services, or HTTP abstractions are wired into the test project; dependency injection for components (e.g., `IApiClient`, `ILocalStorageService`, `IDialogService`) is unmocked.
|
||||||
|
- CI expectations for UI regression coverage are unclear; codifying a baseline will help future contributors.
|
||||||
|
|
||||||
|
## High‑Level Strategy
|
||||||
|
1. **Lay the foundation**: add bUnit/MudBlazor testing dependencies, create reusable test context helpers, and introduce typed doubles for frequently injected services.
|
||||||
|
2. **Cover critical views first**: prioritise pages and components with complex state or upcoming rewrites (Search page, Torrent list, dialogs).
|
||||||
|
3. **Expand outward**: incrementally add tests for navigation/layout wrappers, filter components, and shared dialogs as new features land.
|
||||||
|
4. **Integrate with CI**: ensure `dotnet test` executes component tests locally and on pipelines, with fixtures structured for parallel execution.
|
||||||
|
5. **Adopt guardrails**: document patterns and required assertions so new components ship with tests by default.
|
||||||
|
|
||||||
|
## Implementation Steps
|
||||||
|
|
||||||
|
### 1. Test Project Setup
|
||||||
|
- Update `Lantean.QBTMud.Test.csproj`:
|
||||||
|
- Add packages: `bunit`, `bunit.xunit`, `Bunit.Moq`, `MudBlazor.Services`, `Microsoft.Extensions.DependencyInjection`, `Moq`, and `AwesomeAssertions`.
|
||||||
|
- Enable nullable warnings consistency by mirroring app project settings.
|
||||||
|
- Create a `TestImports.cs` file with global usings for bUnit, MudBlazor, Moq/NSubstitute, and the app namespaces to reduce boilerplate.
|
||||||
|
- Replace existing placeholder tests with a `SmokeTests` folder reserved for minimal sanity checks.
|
||||||
|
|
||||||
|
### 2. Shared Test Infrastructure
|
||||||
|
- Introduce `ComponentTestContext : TestContext` (or extension methods) under `Lantean.QBTMud.Test/Infrastructure` to centralise DI setup:
|
||||||
|
- Register MudBlazor services (`Services.AddMudServices()`), NavigationManager (FakeNav), configuration, and logging stubs.
|
||||||
|
- Provide helper `AddApiClientMock`, `AddLocalStorageMock`, etc., returning strongly typed mocks or substitutes.
|
||||||
|
- Ensure deterministic `ILocalStorageService` by using `Blazored.LocalStorage`’s in-memory implementation or a bespoke stub.
|
||||||
|
- Expose utility methods (`RenderComponentWithServices<TComponent>(Action<IServiceCollection>?)`) so tests can override specific dependencies.
|
||||||
|
- Add snapshot helpers for table row extraction, dialog inspection, and event dispatch (e.g., clicking buttons, submitting forms).
|
||||||
|
|
||||||
|
### 3. Search Page Coverage (High Priority)
|
||||||
|
- Create `Search` test suite (aligning with Search-Implementation plan):
|
||||||
|
- **Form rendering**: assert initial state (default plugin/category selection, button text) and dynamic behaviour (Stop label once a job starts).
|
||||||
|
- **Search lifecycle**: mock `IApiClient` to return synthetic plugin lists, search IDs, and results. Validate that `StartSearch` is called with expected payloads and that subsequent renders display fetched rows.
|
||||||
|
- **Job management UI**: when multi-job support ships, verify tab/list rendering, status icons, and ability to stop/delete jobs on user interaction.
|
||||||
|
- **Client-side filters**: stub job results and assert that seeds/size/search-scope filters adjust the rendered rows and visible totals.
|
||||||
|
- **Context menu actions**: simulate row right-click and ensure download/copy handlers invoke the right helper methods (`DialogHelper`, clipboard service).
|
||||||
|
|
||||||
|
### 4. Torrent List & Filters
|
||||||
|
- Cover `Pages/TorrentList.razor` with focus on:
|
||||||
|
- Toolbar state (search box debounce, filter chips, action menus).
|
||||||
|
- Interaction with cascaded `MainData` and `SearchTermChanged` callbacks.
|
||||||
|
- Row selection + bulk action context menus (mock API calls via injected services).
|
||||||
|
- Add tests for `Components/FiltersNav.razor` verifying bucket counts, selection, and tracker/category pipes once filter logic is upgraded.
|
||||||
|
- Validate `FilterHelper` behaviours via dedicated unit tests if not already covered (regex toggle, field selection, status buckets).
|
||||||
|
|
||||||
|
### 5. Dialog & Action Components
|
||||||
|
- For each Mud dialog (e.g., `AddTorrentFileDialog`, `ColumnOptionsDialog`, upcoming `SearchPluginsDialog`):
|
||||||
|
- Render inside a `DialogService` test host, populate parameters, trigger submission, and assert returned `DialogResult` data.
|
||||||
|
- Mock `IApiClient` interactions (upload torrent, enable plugin). Ensure failures surface error UI (snackbar/toasts) when applicable.
|
||||||
|
- Test `DialogHelper` extension methods by invoking them within the test context and verifying underlying service calls.
|
||||||
|
|
||||||
|
### 6. Layout & Navigation
|
||||||
|
- Test `Layout/LoggedInLayout.razor` and `Layout/ListLayout.razor` for:
|
||||||
|
- Drawer toggling logic, search cascades, and navigation events (`NavigationManager.NavigateTo`).
|
||||||
|
- Correct propagation of `CascadingValue`s to child components using a stub child that records received values.
|
||||||
|
- Ensure top-level routes (e.g., `/`, `/search`, `/settings`) render expected components via `Router` tests or minimal `App.razor` integration tests.
|
||||||
|
|
||||||
|
### 7. Regression Harness & Tooling
|
||||||
|
- Configure `dotnet test` to run with `--filter "FullyQualifiedName~Lantean.QBTMud"` to focus on component tests during local workflows; optionally add a separate github action job for UI tests.
|
||||||
|
- Implement deterministic snapshot helpers (HTML normalisation) only if comparisons are stable; otherwise rely on semantic assertions (CSS class presence, text, event invocation).
|
||||||
|
- Document new testing conventions in `CONTRIBUTING.md` or a dedicated `docs/testing.md` entry (how to add bUnit tests, service registration patterns, use of mocks).
|
||||||
|
|
||||||
|
## Component Prioritisation Checklist
|
||||||
|
1. **Critical flows**: Search page, Torrent list, Add torrent dialogs, Share ratio dialog.
|
||||||
|
2. **High churn components**: Filters, status navigation, upcoming tracker changes.
|
||||||
|
3. **Shared UI primitives**: `DynamicTable`, `FieldSwitch`, `SortLabel`—ensure core behaviours (sorting, column selection, local storage state) are verified.
|
||||||
|
4. **Error states**: offline mode (`MainData.LostConnection`), failed API calls, and empty lists (no torrents, no plugins).
|
||||||
|
|
||||||
|
## Testing Utilities to Build
|
||||||
|
- `ApiClientMockBuilder`: fluent helper returning mocks with queued responses for search/torrent operations.
|
||||||
|
- `LocalStorageInMemory`: simple implementation capturing set/get, supporting assertions on persisted keys (column selections, search filters).
|
||||||
|
- `EventDispatcher`: wraps `IRenderedComponent<T>` to simplify firing click/submit/change events on MudBlazor controls (abstracts CSS selectors).
|
||||||
|
- `DialogHostDriver`: orchestrates rendering a dialog and extracting returned data without duplicating boilerplate.
|
||||||
|
|
||||||
|
## Deliverables & Milestones
|
||||||
|
1. **Sprint 1**: project setup, base infrastructure, smoke test rendering of home/search pages.
|
||||||
|
2. **Sprint 2**: full coverage for Search page (form, lifecycle, filters) with mocked API flows.
|
||||||
|
3. **Sprint 3**: torrent list + filters + column options dialog tests; measure coverage delta.
|
||||||
|
4. **Sprint 4**: dialogs/actions (add torrent, share ratio, plugin management), plus regression fixtures for navigation layouts.
|
||||||
|
5. **Ongoing**: integrate with CI, enforce new component tests as part of definition of done.
|
||||||
|
|
||||||
|
## Open Questions / Assumptions
|
||||||
|
- Determine preferred mocking framework (current packages include AwesomeAssertions; decide whether to standardise on Moq or NSubstitute).
|
||||||
|
- Confirm availability of clipboard/browser APIs within test environment; may need to wrap them for deterministic testing.
|
||||||
|
- Decide on snapshot vs semantic assertions for DynamicTable output—HTML may be verbose; consider helper methods to parse table rows into POCOs before asserting.
|
||||||
|
- Validate whether UI tests must run under multiple cultures/themes; if so, extend test context to toggle `MudTheme` or culture info.
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
- Review and align on tooling choices (Moq vs NSubstitute, FluentAssertions adoption).
|
||||||
|
- Implement Step 1–2 in a feature branch, replacing placeholder tests with the shared infrastructure and a first Search page smoke test.
|
||||||
|
- Iterate on the checklist as new UI work (Search parity, tracker filters) lands to keep tests in lockstep with features.
|
||||||
|
|
||||||
25
Upgrade-To-v5-Planning.md
Normal file
25
Upgrade-To-v5-Planning.md
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
# Upgrade to qBittorrent WebUI v5 – UI Alignment Plan
|
||||||
|
|
||||||
|
## ~~Torrent List Filtering~~
|
||||||
|
- ~~**Regex toggle & field selector**: Introduce the regex checkbox and the "Filter by" (Name/Save path) select found in v5. Update `FilterState`/`LoggedInLayout` to carry both values, wire them to `TorrentList`’s toolbar, and validate invalid patterns gracefully.~~
|
||||||
|
- ~~**Filter helper parity**: Rework `FilterHelper.ContainsAllTerms/FilterTerms` to mirror `window.qBittorrent.Misc.containsAllTerms` (evaluate every term, respect `+`/`-` prefixes). Ensure filtering applies to the selected field, not just the torrent name.~~
|
||||||
|
- ~~**New status buckets**: Add `Running` and `Moving` to `Status` enum, update `FilterHelper.FilterStatus`, `DisplayHelpers`, and `FiltersNav` so counts/icons match upstream.~~
|
||||||
|
|
||||||
|
## ~~Tracker Filters~~
|
||||||
|
- ~~**Special buckets**: Extend `FilterHelper`/`DataManager` to create sets for "Announce error", "Error", "Warning", and "Trackerless" in addition to "All". Store the required flags on the UI `Torrent` model (`HasTrackerError`, `HasTrackerWarning`, `HasOtherAnnounceError`, `TrackersCount`, etc.).~~
|
||||||
|
- ~~**Tracker grouping & removal**: When grouping trackers by host in `FiltersNav`, retain original URL entries so removal can target the right string. Replace the placeholder "Remove tracker" action with a real implementation and disable it for synthetic buckets.~~
|
||||||
|
|
||||||
|
## ~~Torrent Data Model & Columns~~
|
||||||
|
- ~~**Model sync**: Bring `Lantean.QBTMud.Models.Torrent` into parity with v5 (`Popularity`, `DownloadPath`, `RootPath`, `InfoHashV1/2`, `IsPrivate`, share-limit action fields, tracker flags, etc.) and map them in `DataManager.CreateTorrent`.~~
|
||||||
|
- ~~**Column set alignment**: Match the v5 table defaults—add missing columns (`Popularity`, `Reannounce` in, `Info` hashes, `Download path`, `Private`, etc.), fix "Ratio Limit" to display `RatioLimit`, and ensure column ordering/enabled state mirrors `DynamicTable.TorrentsTable`.~~
|
||||||
|
- ~~**Helper updates**: Extend `DisplayHelpers` to format the new fields (popularity, private flag, info hashes, error state icons).~~
|
||||||
|
|
||||||
|
## Actions & Dialogs
|
||||||
|
- ~~**Copy submenu**: Add "Copy comment" and "Copy content path" to the copy submenu in `TorrentActions`, keeping clipboard behaviour identical to v5.~~
|
||||||
|
- ~~**Share ratio dialog**: Update `ShareRatioDialog`, `ShareRatio/ShareRatioMax`, and `DialogHelper.InvokeShareRatioDialog` to surface `ShareLimitAction`, fix the `MaxInactiveSeedingTime` mapping, and call `SetTorrentShareLimit` with the action.~~
|
||||||
|
|
||||||
|
## ~~Add-Torrent Flow~~
|
||||||
|
- ~~Mirror the v5 add-torrent pane: add controls for incomplete save path, tags, auto-start, queue position, share-limit action, etc., in `AddTorrentOptions.razor`, and wire the new fields into the submission object.~~
|
||||||
|
|
||||||
|
## ~~Preferences & Local Settings~~
|
||||||
|
- ~~Introduce new v5 toggles such as "Display full tracker URL" in `AdvancedOptions`, persist them via the preferences service, and respect the setting in the tracker column rendering.~~
|
||||||
79
src/AGENTS.md
Normal file
79
src/AGENTS.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Code Generation Rules
|
||||||
|
|
||||||
|
## Expectations
|
||||||
|
- Code must be technically precise, unambiguous, and avoid bad practices.
|
||||||
|
- Keep code consistent with the coding standards below.
|
||||||
|
- Follow Microsoft's official best practices for C#, Razor, and SQL.
|
||||||
|
- Adhere to SOLID and DRY principles.
|
||||||
|
- Avoid security vulnerabilities and common pitfalls.
|
||||||
|
- Write clean, self-documenting, readable code; use inline comments only where needed.
|
||||||
|
- Always include XML documentation on public methods with `<summary>`, `<param>`, and `<returns>` tags.
|
||||||
|
- Structure error and exception messages clearly, with correct grammar and punctuation.
|
||||||
|
- Design thoughtfully with proper async usage, memory safety, and dependency injection.
|
||||||
|
- Prioritize maintainability, testability, and scalability.
|
||||||
|
|
||||||
|
## Coding Standards
|
||||||
|
|
||||||
|
### Naming
|
||||||
|
- Use PascalCase for classes, records, structs, methods, properties, and public fields.
|
||||||
|
- Use _camelCase for private fields and private constants.
|
||||||
|
- Use PascalCase for public constants.
|
||||||
|
- Use camelCase for local variables and methods.
|
||||||
|
- Interfaces must begin with `I`.
|
||||||
|
|
||||||
|
### Formatting
|
||||||
|
- Braces on a new line and never omitted.
|
||||||
|
- Use blank lines where appropriate to improve readability.
|
||||||
|
- Expression-bodied members are allowed only for get-only properties; methods must use block bodies.
|
||||||
|
- Member order:
|
||||||
|
1. Constants
|
||||||
|
2. Static properties/fields
|
||||||
|
3. Private fields
|
||||||
|
4. Private properties
|
||||||
|
5. Public fields
|
||||||
|
6. Public properties
|
||||||
|
7. Constructor
|
||||||
|
8. Public instance methods
|
||||||
|
9. Private instance methods
|
||||||
|
10. Public static methods
|
||||||
|
11. Private static methods
|
||||||
|
|
||||||
|
### Coding Practices
|
||||||
|
- Use `var` wherever possible unless it harms clarity.
|
||||||
|
- Enable and properly use nullable reference types.
|
||||||
|
- Always specify access modifiers, even when the default applies.
|
||||||
|
- Use `async` only when needed; append `Async` only if a synchronous counterpart exists.
|
||||||
|
- Prefer LINQ for simple operations; use loops for complex logic.
|
||||||
|
- Do not use exceptions for flow control.
|
||||||
|
|
||||||
|
### Design
|
||||||
|
- Use constructor injection only, unless absolutely necessary (for example, in Blazor).
|
||||||
|
- Static methods and classes are acceptable when appropriate.
|
||||||
|
- Avoid partial classes in user code unless generated.
|
||||||
|
- Use `record` for data-only objects.
|
||||||
|
- Extension methods are permitted and should follow standard naming conventions.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- XML documentation comments are required on all public methods:
|
||||||
|
- Include `<summary>`, `<param>` (if applicable), and `<returns>` (when needed).
|
||||||
|
- Use inline comments sparingly and only to explain complex or non-obvious logic.
|
||||||
|
- Place attributes one per line.
|
||||||
|
- Only one type per file; the file name must match the type.
|
||||||
|
- Exception: multiple generic variants of the same type may share a file if small and strongly related.
|
||||||
|
|
||||||
|
## Enforcement
|
||||||
|
- Generate C# code that follows these standards exactly.
|
||||||
|
- If existing code does not follow these rules, call it out explicitly before proceeding.
|
||||||
|
|
||||||
|
## Pre-flight checklist (agents must confirm)
|
||||||
|
- [ ] Standards here are applied to all generated code.
|
||||||
|
- [ ] Nullable reference types are enabled and used correctly.
|
||||||
|
- [ ] Public methods include XML docs with proper tags.
|
||||||
|
- [ ] Braces are never omitted; no expression-bodied methods (except get-only properties).
|
||||||
|
- [ ] Async usage is justified; `Async` suffix only when a sync counterpart exists.
|
||||||
|
- [ ] Member order matches the specified list.
|
||||||
|
- [ ] Access modifiers are explicit everywhere.
|
||||||
|
- [ ] LINQ used for simple ops; loops for complex logic.
|
||||||
|
- [ ] No exceptions are used for flow control.
|
||||||
|
- [ ] Design follows DI, SOLID, DRY; security pitfalls avoided.
|
||||||
|
- [ ] Any conflicts with existing code are reported for clarification.
|
||||||
121
src/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor
Normal file
121
src/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
@using Lantean.QBitTorrentClient.Models
|
||||||
|
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<MudCollapse Expanded="Expanded">
|
||||||
|
<MudGrid Class="mt-2">
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="bool" Label="Torrent management mode" Value="@TorrentManagementMode" ValueChanged="@SetTorrentManagementMode" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@false">Manual</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@true">Automatic</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudTextField T="string" Label="Save files to location" Value="@SavePath" ValueChanged="@SavePathChanged" Variant="Variant.Outlined" Disabled="@TorrentManagementMode" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<FieldSwitch Label="Use incomplete save path" Value="@UseDownloadPath" ValueChanged="@SetUseDownloadPath" Disabled="@TorrentManagementMode" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField T="string" Label="Incomplete save path" Value="@DownloadPath" ValueChanged="@DownloadPathChanged" Variant="Variant.Outlined" Disabled="@DownloadPathDisabled" />
|
||||||
|
</MudItem>
|
||||||
|
@if (ShowCookieOption)
|
||||||
|
{
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
}
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Category" Value="@Category" ValueChanged="@CategoryChanged" Variant="Variant.Outlined" Clearable="true">
|
||||||
|
<MudSelectItem Value="@string.Empty">None</MudSelectItem>
|
||||||
|
@foreach (var category in CategoryOptions)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@category.Name">@category.Name</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Tags" Variant="Variant.Outlined" MultiSelection="true" SelectedValues="@SelectedTags" SelectedValuesChanged="@SelectedTagsChanged" Disabled="@(AvailableTags.Count == 0)">
|
||||||
|
@foreach (var tag in AvailableTags)
|
||||||
|
{
|
||||||
|
<MudSelectItem Value="@tag">@tag</MudSelectItem>
|
||||||
|
}
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Start torrent" @bind-Value="StartTorrent" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Stop condition" Value="@StopCondition" ValueChanged="@StopConditionChanged" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@("None")">None</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="string" Label="Content layout" Value="@ContentLayout" ValueChanged="@ContentLayoutChanged" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Download in sequential order" @bind-Value="DownloadInSequentialOrder" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="6">
|
||||||
|
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitMode" Label="Share limit preset" Value="@SelectedShareLimitMode" ValueChanged="@ShareLimitModeChanged" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.Global">Use global share limit</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.NoLimit">Set no share limit</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitMode.Custom">Set custom share limit</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Ratio" Value="@RatioLimitEnabled" ValueChanged="@RatioLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="float" Label="Ratio limit" Value="@RatioLimit" ValueChanged="@RatioLimitChanged" Disabled="@(!RatioLimitEnabled || !IsCustomShareLimit)" Min="0" Step="0.1f" Format="F2" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Total minutes" Value="@SeedingTimeLimitEnabled" ValueChanged="@SeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="int" Label="Total minutes" Value="@SeedingTimeLimit" ValueChanged="@SeedingTimeLimitChanged" Disabled="@(!SeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="4">
|
||||||
|
<FieldSwitch Label="Inactive minutes" Value="@InactiveSeedingTimeLimitEnabled" ValueChanged="@InactiveSeedingTimeLimitEnabledChanged" Disabled="@(!IsCustomShareLimit)" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" sm="8">
|
||||||
|
<MudNumericField T="int" Label="Inactive minutes" Value="@InactiveSeedingTimeLimit" ValueChanged="@InactiveSeedingTimeLimitChanged" Disabled="@(!InactiveSeedingTimeLimitEnabled || !IsCustomShareLimit)" Min="1" Variant="Variant.Outlined" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="@SelectedShareLimitAction" ValueChanged="@ShareLimitActionChanged" Disabled="@(!IsCustomShareLimit)" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Default">Default</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Stop">Stop torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.Remove">Remove torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
|
||||||
|
<MudSelectItem Value="@ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudCollapse>
|
||||||
440
src/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs
Normal file
440
src/Lantean.QBTMud/Components/Dialogs/AddTorrentOptions.razor.cs
Normal file
@@ -0,0 +1,440 @@
|
|||||||
|
using Lantean.QBitTorrentClient;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using Lantean.QBTMud.Models;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Components.Dialogs
|
||||||
|
{
|
||||||
|
public partial class AddTorrentOptions
|
||||||
|
{
|
||||||
|
private readonly List<CategoryOption> _categoryOptions = new();
|
||||||
|
private readonly Dictionary<string, CategoryOption> _categoryLookup = new(StringComparer.Ordinal);
|
||||||
|
private string _manualSavePath = string.Empty;
|
||||||
|
private bool _manualUseDownloadPath;
|
||||||
|
private string _manualDownloadPath = string.Empty;
|
||||||
|
private string _defaultSavePath = string.Empty;
|
||||||
|
private string _defaultDownloadPath = string.Empty;
|
||||||
|
private bool _defaultDownloadPathEnabled;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool ShowCookieOption { get; set; }
|
||||||
|
|
||||||
|
protected bool Expanded { get; set; }
|
||||||
|
|
||||||
|
protected bool TorrentManagementMode { get; set; }
|
||||||
|
|
||||||
|
protected string SavePath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected string DownloadPath { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected bool UseDownloadPath { get; set; }
|
||||||
|
|
||||||
|
protected bool DownloadPathDisabled => TorrentManagementMode || !UseDownloadPath;
|
||||||
|
|
||||||
|
protected string? Cookie { get; set; }
|
||||||
|
|
||||||
|
protected string? RenameTorrent { get; set; }
|
||||||
|
|
||||||
|
protected IReadOnlyList<CategoryOption> CategoryOptions => _categoryOptions;
|
||||||
|
|
||||||
|
protected string? Category { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
protected List<string> AvailableTags { get; private set; } = [];
|
||||||
|
|
||||||
|
protected HashSet<string> SelectedTags { get; private set; } = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
|
protected bool StartTorrent { get; set; } = true;
|
||||||
|
|
||||||
|
protected bool AddToTopOfQueue { get; set; } = true;
|
||||||
|
|
||||||
|
protected string StopCondition { get; set; } = "None";
|
||||||
|
|
||||||
|
protected bool SkipHashCheck { get; set; }
|
||||||
|
|
||||||
|
protected string ContentLayout { get; set; } = "Original";
|
||||||
|
|
||||||
|
protected bool DownloadInSequentialOrder { get; set; }
|
||||||
|
|
||||||
|
protected bool DownloadFirstAndLastPiecesFirst { get; set; }
|
||||||
|
|
||||||
|
protected long DownloadLimit { get; set; }
|
||||||
|
|
||||||
|
protected long UploadLimit { get; set; }
|
||||||
|
|
||||||
|
protected ShareLimitMode SelectedShareLimitMode { get; set; } = ShareLimitMode.Global;
|
||||||
|
|
||||||
|
protected bool RatioLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected float RatioLimit { get; set; } = 1.0f;
|
||||||
|
|
||||||
|
protected bool SeedingTimeLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected int SeedingTimeLimit { get; set; } = 1440;
|
||||||
|
|
||||||
|
protected bool InactiveSeedingTimeLimitEnabled { get; set; }
|
||||||
|
|
||||||
|
protected int InactiveSeedingTimeLimit { get; set; } = 1440;
|
||||||
|
|
||||||
|
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
|
||||||
|
|
||||||
|
protected bool IsCustomShareLimit => SelectedShareLimitMode == ShareLimitMode.Custom;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
var categories = await ApiClient.GetAllCategories();
|
||||||
|
foreach (var (name, value) in categories.OrderBy(kvp => kvp.Key, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var option = new CategoryOption(name, value.SavePath, value.DownloadPath);
|
||||||
|
_categoryOptions.Add(option);
|
||||||
|
_categoryLookup[name] = option;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tags = await ApiClient.GetAllTags();
|
||||||
|
AvailableTags = tags.OrderBy(t => t, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
var preferences = await ApiClient.GetApplicationPreferences();
|
||||||
|
|
||||||
|
TorrentManagementMode = preferences.AutoTmmEnabled;
|
||||||
|
|
||||||
|
_defaultSavePath = preferences.SavePath ?? string.Empty;
|
||||||
|
_manualSavePath = _defaultSavePath;
|
||||||
|
SavePath = _defaultSavePath;
|
||||||
|
|
||||||
|
_defaultDownloadPath = preferences.TempPath ?? string.Empty;
|
||||||
|
_defaultDownloadPathEnabled = preferences.TempPathEnabled;
|
||||||
|
_manualDownloadPath = _defaultDownloadPath;
|
||||||
|
_manualUseDownloadPath = preferences.TempPathEnabled;
|
||||||
|
UseDownloadPath = _manualUseDownloadPath;
|
||||||
|
DownloadPath = UseDownloadPath ? _manualDownloadPath : string.Empty;
|
||||||
|
|
||||||
|
StartTorrent = !preferences.AddStoppedEnabled;
|
||||||
|
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
||||||
|
StopCondition = preferences.TorrentStopCondition;
|
||||||
|
ContentLayout = preferences.TorrentContentLayout;
|
||||||
|
|
||||||
|
RatioLimitEnabled = preferences.MaxRatioEnabled;
|
||||||
|
RatioLimit = preferences.MaxRatio;
|
||||||
|
SeedingTimeLimitEnabled = preferences.MaxSeedingTimeEnabled;
|
||||||
|
if (preferences.MaxSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
SeedingTimeLimit = preferences.MaxSeedingTime;
|
||||||
|
}
|
||||||
|
InactiveSeedingTimeLimitEnabled = preferences.MaxInactiveSeedingTimeEnabled;
|
||||||
|
if (preferences.MaxInactiveSeedingTimeEnabled)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimit = preferences.MaxInactiveSeedingTime;
|
||||||
|
}
|
||||||
|
SelectedShareLimitAction = MapShareLimitAction(preferences.MaxRatioAct);
|
||||||
|
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetTorrentManagementMode(bool value)
|
||||||
|
{
|
||||||
|
if (TorrentManagementMode == value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
TorrentManagementMode = value;
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
RestoreManualPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SavePathChanged(string value)
|
||||||
|
{
|
||||||
|
SavePath = value;
|
||||||
|
if (!TorrentManagementMode)
|
||||||
|
{
|
||||||
|
_manualSavePath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SetUseDownloadPath(bool value)
|
||||||
|
{
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_manualUseDownloadPath = value;
|
||||||
|
UseDownloadPath = value;
|
||||||
|
|
||||||
|
if (value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_manualDownloadPath))
|
||||||
|
{
|
||||||
|
_manualDownloadPath = string.IsNullOrWhiteSpace(_defaultDownloadPath) ? string.Empty : _defaultDownloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
DownloadPath = _manualDownloadPath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_manualDownloadPath = DownloadPath;
|
||||||
|
DownloadPath = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void DownloadPathChanged(string value)
|
||||||
|
{
|
||||||
|
DownloadPath = value;
|
||||||
|
if (!TorrentManagementMode && UseDownloadPath)
|
||||||
|
{
|
||||||
|
_manualDownloadPath = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CategoryChanged(string? value)
|
||||||
|
{
|
||||||
|
Category = string.IsNullOrWhiteSpace(value) ? null : value;
|
||||||
|
if (TorrentManagementMode)
|
||||||
|
{
|
||||||
|
ApplyAutomaticPaths();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SelectedTagsChanged(IEnumerable<string> tags)
|
||||||
|
{
|
||||||
|
SelectedTags = tags is null
|
||||||
|
? new HashSet<string>(StringComparer.Ordinal)
|
||||||
|
: new HashSet<string>(tags, StringComparer.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void StopConditionChanged(string value)
|
||||||
|
{
|
||||||
|
StopCondition = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ContentLayoutChanged(string value)
|
||||||
|
{
|
||||||
|
ContentLayout = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitModeChanged(ShareLimitMode mode)
|
||||||
|
{
|
||||||
|
SelectedShareLimitMode = mode;
|
||||||
|
if (mode != ShareLimitMode.Custom)
|
||||||
|
{
|
||||||
|
RatioLimitEnabled = false;
|
||||||
|
SeedingTimeLimitEnabled = false;
|
||||||
|
InactiveSeedingTimeLimitEnabled = false;
|
||||||
|
SelectedShareLimitAction = ShareLimitAction.Default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RatioLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
RatioLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void RatioLimitChanged(float value)
|
||||||
|
{
|
||||||
|
RatioLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SeedingTimeLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
SeedingTimeLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void SeedingTimeLimitChanged(int value)
|
||||||
|
{
|
||||||
|
SeedingTimeLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void InactiveSeedingTimeLimitEnabledChanged(bool value)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimitEnabled = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void InactiveSeedingTimeLimitChanged(int value)
|
||||||
|
{
|
||||||
|
InactiveSeedingTimeLimit = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitActionChanged(ShareLimitAction value)
|
||||||
|
{
|
||||||
|
SelectedShareLimitAction = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TorrentOptions GetTorrentOptions()
|
||||||
|
{
|
||||||
|
var options = new TorrentOptions(
|
||||||
|
TorrentManagementMode,
|
||||||
|
_manualSavePath,
|
||||||
|
Cookie,
|
||||||
|
RenameTorrent,
|
||||||
|
string.IsNullOrWhiteSpace(Category) ? null : Category,
|
||||||
|
StartTorrent,
|
||||||
|
AddToTopOfQueue,
|
||||||
|
StopCondition,
|
||||||
|
SkipHashCheck,
|
||||||
|
ContentLayout,
|
||||||
|
DownloadInSequentialOrder,
|
||||||
|
DownloadFirstAndLastPiecesFirst,
|
||||||
|
DownloadLimit,
|
||||||
|
UploadLimit);
|
||||||
|
|
||||||
|
options.UseDownloadPath = TorrentManagementMode ? null : UseDownloadPath;
|
||||||
|
options.DownloadPath = (!TorrentManagementMode && UseDownloadPath) ? DownloadPath : null;
|
||||||
|
options.Tags = SelectedTags.Count > 0 ? SelectedTags.ToArray() : null;
|
||||||
|
|
||||||
|
switch (SelectedShareLimitMode)
|
||||||
|
{
|
||||||
|
case ShareLimitMode.Global:
|
||||||
|
options.RatioLimit = Limits.GlobalLimit;
|
||||||
|
options.SeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
options.ShareLimitAction = ShareLimitAction.Default.ToString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShareLimitMode.NoLimit:
|
||||||
|
options.RatioLimit = Limits.NoLimit;
|
||||||
|
options.SeedingTimeLimit = Limits.NoLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = Limits.NoLimit;
|
||||||
|
options.ShareLimitAction = ShareLimitAction.Default.ToString();
|
||||||
|
break;
|
||||||
|
|
||||||
|
case ShareLimitMode.Custom:
|
||||||
|
options.RatioLimit = RatioLimitEnabled ? RatioLimit : Limits.NoLimit;
|
||||||
|
options.SeedingTimeLimit = SeedingTimeLimitEnabled ? SeedingTimeLimit : Limits.NoLimit;
|
||||||
|
options.InactiveSeedingTimeLimit = InactiveSeedingTimeLimitEnabled ? InactiveSeedingTimeLimit : Limits.NoLimit;
|
||||||
|
options.ShareLimitAction = SelectedShareLimitAction.ToString();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApplyAutomaticPaths()
|
||||||
|
{
|
||||||
|
SavePath = ResolveAutomaticSavePath();
|
||||||
|
var (enabled, path) = ResolveAutomaticDownloadPath();
|
||||||
|
UseDownloadPath = enabled;
|
||||||
|
DownloadPath = enabled ? path ?? string.Empty : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RestoreManualPaths()
|
||||||
|
{
|
||||||
|
SavePath = _manualSavePath;
|
||||||
|
UseDownloadPath = _manualUseDownloadPath;
|
||||||
|
DownloadPath = _manualUseDownloadPath ? _manualDownloadPath : string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ResolveAutomaticSavePath()
|
||||||
|
{
|
||||||
|
var category = GetSelectedCategory();
|
||||||
|
if (category is null)
|
||||||
|
{
|
||||||
|
return _defaultSavePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(category.SavePath))
|
||||||
|
{
|
||||||
|
return category.SavePath!;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(_defaultSavePath) && !string.IsNullOrWhiteSpace(category.Name))
|
||||||
|
{
|
||||||
|
return Path.Combine(_defaultSavePath, category.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
return _defaultSavePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (bool Enabled, string? Path) ResolveAutomaticDownloadPath()
|
||||||
|
{
|
||||||
|
var category = GetSelectedCategory();
|
||||||
|
if (category is null)
|
||||||
|
{
|
||||||
|
if (!_defaultDownloadPathEnabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, _defaultDownloadPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category.DownloadPath is null)
|
||||||
|
{
|
||||||
|
if (!_defaultDownloadPathEnabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, ComposeDefaultDownloadPath(category.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!category.DownloadPath.Enabled)
|
||||||
|
{
|
||||||
|
return (false, string.Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(category.DownloadPath.Path))
|
||||||
|
{
|
||||||
|
return (true, category.DownloadPath.Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (true, ComposeDefaultDownloadPath(category.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
private string ComposeDefaultDownloadPath(string categoryName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(_defaultDownloadPath))
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(categoryName))
|
||||||
|
{
|
||||||
|
return _defaultDownloadPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(_defaultDownloadPath, categoryName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private CategoryOption? GetSelectedCategory()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(Category))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _categoryLookup.TryGetValue(Category, out var option) ? option : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ShareLimitAction MapShareLimitAction(int preferenceValue)
|
||||||
|
{
|
||||||
|
return preferenceValue switch
|
||||||
|
{
|
||||||
|
0 => ShareLimitAction.Stop,
|
||||||
|
1 => ShareLimitAction.Remove,
|
||||||
|
2 => ShareLimitAction.RemoveWithContent,
|
||||||
|
3 => ShareLimitAction.EnableSuperSeeding,
|
||||||
|
_ => ShareLimitAction.Default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected enum ShareLimitMode
|
||||||
|
{
|
||||||
|
Global,
|
||||||
|
NoLimit,
|
||||||
|
Custom
|
||||||
|
}
|
||||||
|
|
||||||
|
protected sealed record CategoryOption(string Name, string? SavePath, DownloadPathOption? DownloadPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
@@ -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>
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
126
src/Lantean.QBTMud/Components/Dialogs/SearchPluginsDialog.razor
Normal file
126
src/Lantean.QBTMud/Components/Dialogs/SearchPluginsDialog.razor
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<MudDialog Class="search-plugins-dialog">
|
||||||
|
<DialogContent>
|
||||||
|
@if (IsBusy)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
|
||||||
|
}
|
||||||
|
<MudGrid Class="mb-4" Spacing="2">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Install from URL</MudText>
|
||||||
|
<MudTextField @bind-Value="InstallUrl"
|
||||||
|
Placeholder="https://example.com/plugin.zip"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
Immediate="true" />
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Disabled="OperationInProgress || string.IsNullOrWhiteSpace(InstallUrl)"
|
||||||
|
OnClick="InstallFromUrl"
|
||||||
|
Class="mt-3"
|
||||||
|
StartIcon="@Icons.Material.Filled.CloudDownload">Install</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Install from server path</MudText>
|
||||||
|
<MudTextField @bind-Value="InstallLocalPath"
|
||||||
|
Placeholder="/home/qbt/plugins/my-plugin.py"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
Immediate="true" />
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Disabled="OperationInProgress || string.IsNullOrWhiteSpace(InstallLocalPath)"
|
||||||
|
OnClick="InstallFromPath"
|
||||||
|
Class="mt-3"
|
||||||
|
StartIcon="@Icons.Material.Filled.UploadFile">Install</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<MudPaper Class="pa-3 mb-3">
|
||||||
|
<MudStack Row="true" Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.ToggleOn"
|
||||||
|
OnClick="EnableSelected">Enable</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.ToggleOff"
|
||||||
|
OnClick="DisableSelected">Disable</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Error"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.Delete"
|
||||||
|
OnClick="UninstallSelected">Uninstall</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Info"
|
||||||
|
Disabled="OperationInProgress || Plugins.Count == 0"
|
||||||
|
StartIcon="@Icons.Material.Filled.Update"
|
||||||
|
OnClick="UpdateAll">Update all</MudButton>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudTooltip Text="Refresh plugins">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||||
|
Color="Color.Default"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="RefreshPlugins" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
<MudTable T="Lantean.QBitTorrentClient.Models.SearchPlugin"
|
||||||
|
Items="Plugins"
|
||||||
|
Hover="true"
|
||||||
|
Bordered="true"
|
||||||
|
Dense="true">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Select</MudTh>
|
||||||
|
<MudTh>Enabled</MudTh>
|
||||||
|
<MudTh>Name</MudTh>
|
||||||
|
<MudTh>Identifier</MudTh>
|
||||||
|
<MudTh>Version</MudTh>
|
||||||
|
<MudTh>Last update</MudTh>
|
||||||
|
<MudTh>Source</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Select">
|
||||||
|
<MudIconButton Icon="@GetSelectionIcon(context)"
|
||||||
|
Color="@GetSelectionColor(context)"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="@(() => ToggleSelection(context))" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Enabled">
|
||||||
|
<MudIconButton Icon="@GetEnabledIcon(context)"
|
||||||
|
Color="@GetEnabledColor(context)"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="@(() => TogglePlugin(context, !context.Enabled))" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Name">
|
||||||
|
<MudText Typo="Typo.body2">@context.FullName</MudText>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Identifier">
|
||||||
|
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Outlined" Size="Size.Small">@context.Name</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Version">@context.Version</MudTd>
|
||||||
|
<MudTd DataLabel="Last update">@GetLastUpdatedText(context)</MudTd>
|
||||||
|
<MudTd DataLabel="Source">
|
||||||
|
@if (string.IsNullOrWhiteSpace(context.Url))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption">Not provided</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudLink Href="@context.Url" Target="_blank">@context.Url</MudLink>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="pa-4">No plugins installed.</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="CloseDialog">Close</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
@@ -0,0 +1,241 @@
|
|||||||
|
using System.Linq;
|
||||||
|
using System.Net.Http;
|
||||||
|
using Lantean.QBitTorrentClient;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
|
using Microsoft.AspNetCore.Components;
|
||||||
|
using MudBlazor;
|
||||||
|
|
||||||
|
namespace Lantean.QBTMud.Components.Dialogs
|
||||||
|
{
|
||||||
|
public partial class SearchPluginsDialog
|
||||||
|
{
|
||||||
|
[Inject]
|
||||||
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected ISnackbar Snackbar { get; set; } = default!;
|
||||||
|
|
||||||
|
[CascadingParameter]
|
||||||
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
|
protected List<SearchPlugin> Plugins { get; set; } = [];
|
||||||
|
|
||||||
|
protected HashSet<string> SelectedPluginNames { get; set; } = [];
|
||||||
|
|
||||||
|
protected string? InstallUrl { get; set; }
|
||||||
|
|
||||||
|
protected string? InstallLocalPath { get; set; }
|
||||||
|
|
||||||
|
protected bool OperationInProgress { get; set; }
|
||||||
|
|
||||||
|
private bool _hasChanges;
|
||||||
|
private bool _loading;
|
||||||
|
|
||||||
|
protected bool IsBusy => _loading || OperationInProgress;
|
||||||
|
|
||||||
|
protected bool HasSelection => SelectedPluginNames.Count > 0;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
await LoadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadPlugins()
|
||||||
|
{
|
||||||
|
_loading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await ApiClient.GetSearchPlugins();
|
||||||
|
Plugins = response is null ? [] : response.ToList();
|
||||||
|
SelectedPluginNames = [];
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Failed to load search plugins: {exception.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_loading = false;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task InstallFromUrl()
|
||||||
|
{
|
||||||
|
var source = InstallUrl?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(source))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await RunOperation(() => ApiClient.InstallSearchPlugins(source), "Plugin install queued.", true);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
InstallUrl = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task InstallFromPath()
|
||||||
|
{
|
||||||
|
var source = InstallLocalPath?.Trim();
|
||||||
|
if (string.IsNullOrEmpty(source))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var success = await RunOperation(() => ApiClient.InstallSearchPlugins(source), "Plugin install queued.", true);
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
InstallLocalPath = string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task EnableSelected()
|
||||||
|
{
|
||||||
|
if (!HasSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var names = SelectedPluginNames.ToArray();
|
||||||
|
await RunOperation(() => ApiClient.EnableSearchPlugins(names), $"Enabled {names.Length} plugin(s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task DisableSelected()
|
||||||
|
{
|
||||||
|
if (!HasSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var names = SelectedPluginNames.ToArray();
|
||||||
|
await RunOperation(() => ApiClient.DisableSearchPlugins(names), $"Disabled {names.Length} plugin(s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task UninstallSelected()
|
||||||
|
{
|
||||||
|
if (!HasSelection)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var names = SelectedPluginNames.ToArray();
|
||||||
|
await RunOperation(() => ApiClient.UninstallSearchPlugins(names), $"Removed {names.Length} plugin(s).");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task UpdateAll()
|
||||||
|
{
|
||||||
|
if (Plugins.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await RunOperation(() => ApiClient.UpdateSearchPlugins(), "Plugin update queued.");
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task TogglePlugin(SearchPlugin plugin, bool enable)
|
||||||
|
{
|
||||||
|
if (OperationInProgress)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previous = plugin.Enabled;
|
||||||
|
plugin.Enabled = enable;
|
||||||
|
|
||||||
|
var success = await RunOperation(
|
||||||
|
enable
|
||||||
|
? () => ApiClient.EnableSearchPlugins(plugin.Name)
|
||||||
|
: () => ApiClient.DisableSearchPlugins(plugin.Name),
|
||||||
|
enable ? $"Enabled {plugin.FullName}." : $"Disabled {plugin.FullName}.",
|
||||||
|
false);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
{
|
||||||
|
plugin.Enabled = previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task RefreshPlugins()
|
||||||
|
{
|
||||||
|
await LoadPlugins();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected bool IsSelected(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
return SelectedPluginNames.Contains(plugin.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void ToggleSelection(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
if (!SelectedPluginNames.Add(plugin.Name))
|
||||||
|
{
|
||||||
|
SelectedPluginNames.Remove(plugin.Name);
|
||||||
|
}
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetSelectionIcon(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
return IsSelected(plugin) ? Icons.Material.Filled.CheckBox : Icons.Material.Outlined.CheckBoxOutlineBlank;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Color GetSelectionColor(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
return IsSelected(plugin) ? Color.Primary : Color.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetEnabledIcon(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
return plugin.Enabled ? Icons.Material.Filled.ToggleOn : Icons.Material.Outlined.ToggleOff;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected Color GetEnabledColor(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
return plugin.Enabled ? Color.Success : Color.Default;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected string GetLastUpdatedText(SearchPlugin plugin)
|
||||||
|
{
|
||||||
|
// qBittorrent's search/plugins API does not expose the last update timestamp,
|
||||||
|
// so we display a placeholder until the client model is extended.
|
||||||
|
return "Not available";
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void CloseDialog()
|
||||||
|
{
|
||||||
|
MudDialog.Close(DialogResult.Ok(_hasChanges));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task<bool> RunOperation(Func<Task> operation, string successMessage, bool refresh = true)
|
||||||
|
{
|
||||||
|
OperationInProgress = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await operation();
|
||||||
|
Snackbar.Add(successMessage, Severity.Success);
|
||||||
|
_hasChanges = true;
|
||||||
|
if (refresh)
|
||||||
|
{
|
||||||
|
await LoadPlugins();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (HttpRequestException exception)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Search plugin operation failed: {exception.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException exception)
|
||||||
|
{
|
||||||
|
Snackbar.Add($"Search plugin operation failed: {exception.Message}", Severity.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
OperationInProgress = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
@using Lantean.QBitTorrentClient.Models
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -34,6 +35,15 @@
|
|||||||
<MudItem xs="9">
|
<MudItem xs="9">
|
||||||
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
|
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="SelectedShareLimitAction" ValueChanged="ShareLimitActionChanged" Disabled="@(!CustomEnabled)" Variant="Variant.Outlined">
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Default">Default</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Stop">Stop torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.Remove">Remove torrent</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
|
||||||
|
<MudSelectItem Value="ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
|
||||||
|
</MudSelect>
|
||||||
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Lantean.QBitTorrentClient;
|
||||||
|
using Lantean.QBitTorrentClient.Models;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -16,6 +19,9 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public ShareRatioMax? Value { get; set; }
|
public ShareRatioMax? Value { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public ShareRatioMax? CurrentValue { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool Disabled { get; set; }
|
public bool Disabled { get; set; }
|
||||||
|
|
||||||
@@ -33,6 +39,8 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected int InactiveMinutes { get; set; }
|
protected int InactiveMinutes { get; set; }
|
||||||
|
|
||||||
|
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
|
||||||
|
|
||||||
protected bool CustomEnabled => ShareRatioType == 0;
|
protected bool CustomEnabled => ShareRatioType == 0;
|
||||||
|
|
||||||
protected void RatioEnabledChanged(bool value)
|
protected void RatioEnabledChanged(bool value)
|
||||||
@@ -65,40 +73,75 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
InactiveMinutes = value;
|
InactiveMinutes = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void ShareLimitActionChanged(ShareLimitAction value)
|
||||||
|
{
|
||||||
|
SelectedShareLimitAction = value;
|
||||||
|
}
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (Value is null || Value.RatioLimit == Limits.GlobalLimit && Value.SeedingTimeLimit == Limits.GlobalLimit && Value.InactiveSeedingTimeLimit == Limits.GlobalLimit)
|
RatioEnabled = false;
|
||||||
|
TotalMinutesEnabled = false;
|
||||||
|
InactiveMinutesEnabled = false;
|
||||||
|
|
||||||
|
var baseline = Value ?? CurrentValue;
|
||||||
|
SelectedShareLimitAction = baseline?.ShareLimitAction ?? ShareLimitAction.Default;
|
||||||
|
|
||||||
|
if (baseline is null || baseline.RatioLimit == Limits.GlobalLimit && baseline.SeedingTimeLimit == Limits.GlobalLimit && baseline.InactiveSeedingTimeLimit == Limits.GlobalLimit)
|
||||||
{
|
{
|
||||||
ShareRatioType = Limits.GlobalLimit;
|
ShareRatioType = Limits.GlobalLimit;
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
else if (Value.MaxRatio == Limits.NoLimit && Value.MaxSeedingTime == Limits.NoLimit && Value.MaxInactiveSeedingTime == Limits.NoLimit)
|
|
||||||
|
if (baseline.MaxRatio == Limits.NoLimit && baseline.MaxSeedingTime == Limits.NoLimit && baseline.MaxInactiveSeedingTime == Limits.NoLimit)
|
||||||
{
|
{
|
||||||
ShareRatioType = Limits.NoLimit;
|
ShareRatioType = Limits.NoLimit;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ShareRatioType = 0;
|
||||||
|
|
||||||
|
if (baseline.RatioLimit >= 0)
|
||||||
|
{
|
||||||
|
RatioEnabled = true;
|
||||||
|
Ratio = baseline.RatioLimit;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
ShareRatioType = 0;
|
Ratio = 0;
|
||||||
if (Value.RatioLimit >= 0)
|
}
|
||||||
{
|
|
||||||
RatioEnabled = true;
|
if (baseline.SeedingTimeLimit >= 0)
|
||||||
Ratio = Value.RatioLimit;
|
{
|
||||||
}
|
TotalMinutesEnabled = true;
|
||||||
if (Value.SeedingTimeLimit >= 0)
|
TotalMinutes = (int)baseline.SeedingTimeLimit;
|
||||||
{
|
}
|
||||||
TotalMinutesEnabled = true;
|
else
|
||||||
TotalMinutes = (int)Value.SeedingTimeLimit;
|
{
|
||||||
}
|
TotalMinutes = 0;
|
||||||
if (Value.InactiveSeedingTimeLimit >= 0)
|
}
|
||||||
{
|
|
||||||
InactiveMinutesEnabled = true;
|
if (baseline.InactiveSeedingTimeLimit >= 0)
|
||||||
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
|
{
|
||||||
}
|
InactiveMinutesEnabled = true;
|
||||||
|
InactiveMinutes = (int)baseline.InactiveSeedingTimeLimit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
InactiveMinutes = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void ShareRatioTypeChanged(int value)
|
protected void ShareRatioTypeChanged(int value)
|
||||||
{
|
{
|
||||||
ShareRatioType = value;
|
ShareRatioType = value;
|
||||||
|
if (!CustomEnabled)
|
||||||
|
{
|
||||||
|
RatioEnabled = false;
|
||||||
|
TotalMinutesEnabled = false;
|
||||||
|
InactiveMinutesEnabled = false;
|
||||||
|
SelectedShareLimitAction = ShareLimitAction.Default;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Cancel()
|
protected void Cancel()
|
||||||
@@ -112,16 +155,19 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
if (ShareRatioType == Limits.GlobalLimit)
|
if (ShareRatioType == Limits.GlobalLimit)
|
||||||
{
|
{
|
||||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
|
||||||
|
result.ShareLimitAction = ShareLimitAction.Default;
|
||||||
}
|
}
|
||||||
else if (ShareRatioType == Limits.NoLimit)
|
else if (ShareRatioType == Limits.NoLimit)
|
||||||
{
|
{
|
||||||
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
|
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
|
||||||
|
result.ShareLimitAction = ShareLimitAction.Default;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
|
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
|
||||||
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
|
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
|
||||||
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
|
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
|
||||||
|
result.ShareLimitAction = SelectedShareLimitAction;
|
||||||
}
|
}
|
||||||
MudDialog.Close(DialogResult.Ok(result));
|
MudDialog.Close(DialogResult.Ok(result));
|
||||||
}
|
}
|
||||||
@@ -22,7 +22,7 @@
|
|||||||
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
|
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
|
||||||
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
|
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
|
||||||
<MudSpacer />
|
<MudSpacer />
|
||||||
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
<MudTextField @ref="SearchInput" 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">
|
<div class="content-panel__body">
|
||||||
@@ -50,7 +50,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> ExpandedNodes { get; set; } = [];
|
protected HashSet<string> ExpandedNodes { get; set; } = [];
|
||||||
|
|
||||||
@@ -70,6 +70,8 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
private MudMenu? ContextMenu { get; set; }
|
private MudMenu? ContextMenu { get; set; }
|
||||||
|
|
||||||
|
private MudTextField<string>? SearchInput { get; set; }
|
||||||
|
|
||||||
public FilesTab()
|
public FilesTab()
|
||||||
{
|
{
|
||||||
_columnRenderFragments.Add("Name", NameColumn);
|
_columnRenderFragments.Add("Name", NameColumn);
|
||||||
@@ -235,7 +237,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
{
|
{
|
||||||
MarkFilesDirty();
|
MarkFilesDirty();
|
||||||
PruneSelectionIfMissing();
|
PruneSelectionIfMissing();
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(() =>
|
||||||
|
{
|
||||||
|
SyncSearchTextFromInput();
|
||||||
|
StateHasChanged();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -415,6 +421,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
private ReadOnlyCollection<ContentItem> GetFiles()
|
private ReadOnlyCollection<ContentItem> GetFiles()
|
||||||
{
|
{
|
||||||
|
if (SyncSearchTextFromInput())
|
||||||
|
{
|
||||||
|
_filesDirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (!_filesDirty)
|
if (!_filesDirty)
|
||||||
{
|
{
|
||||||
return _visibleFiles;
|
return _visibleFiles;
|
||||||
@@ -522,6 +533,23 @@ namespace Lantean.QBTMud.Components
|
|||||||
return visible;
|
return visible;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool SyncSearchTextFromInput()
|
||||||
|
{
|
||||||
|
if (SearchInput is null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentValue = SearchInput.Value;
|
||||||
|
if (string.Equals(SearchText, currentValue, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
SearchText = currentValue;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private void MarkFilesDirty()
|
private void MarkFilesDirty()
|
||||||
{
|
{
|
||||||
_filesDirty = true;
|
_filesDirty = true;
|
||||||
@@ -26,7 +26,7 @@
|
|||||||
</MudMenu>
|
</MudMenu>
|
||||||
|
|
||||||
<MudMenu @ref="TrackerContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
|
<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" Disabled="@(!CanRemoveTracker)" OnClick="RemoveTracker">Remove tracker</MudMenuItem>
|
||||||
<MudDivider />
|
<MudDivider />
|
||||||
@TorrentControls(_trackerType)
|
@TorrentControls(_trackerType)
|
||||||
</MudMenu>
|
</MudMenu>
|
||||||
@@ -52,9 +52,9 @@
|
|||||||
}
|
}
|
||||||
</MudNavGroup>
|
</MudNavGroup>
|
||||||
<MudNavGroup Title="Trackers" @bind-Expanded="_trackersExpanded">
|
<MudNavGroup Title="Trackers" @bind-Expanded="_trackersExpanded">
|
||||||
@foreach (var (tracker, count) in Trackers)
|
@foreach (var tracker in Trackers)
|
||||||
{
|
{
|
||||||
<CustomNavLink Class="filter-menu-item" Active="@(Tracker == tracker)" Icon="@Icons.Material.Filled.PinDrop" IconColor="Color.Info" OnClick="@(e => TrackerValueChanged(tracker))" OnContextMenu="@(e => TrackerOnContextMenu(e, tracker))" OnLongPress="@(e => TrackerOnLongPress(e, tracker))">@($"{tracker} ({count})")</CustomNavLink>
|
<CustomNavLink Class="filter-menu-item" Active="@(Tracker == tracker.Key)" Icon="@Icons.Material.Filled.PinDrop" IconColor="Color.Info" OnClick="@(e => TrackerValueChanged(tracker.Key))" OnContextMenu="@(e => TrackerOnContextMenu(e, tracker.Key))" OnLongPress="@(e => TrackerOnLongPress(e, tracker.Key))">@($"{tracker.DisplayName} ({tracker.Count})")</CustomNavLink>
|
||||||
}
|
}
|
||||||
</MudNavGroup>
|
</MudNavGroup>
|
||||||
</MudNavMenu>
|
</MudNavMenu>
|
||||||
@@ -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>
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,9 @@ 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;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components
|
namespace Lantean.QBTMud.Components
|
||||||
{
|
{
|
||||||
@@ -24,6 +27,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
private bool _categoriesExpanded = true;
|
private bool _categoriesExpanded = true;
|
||||||
private bool _tagsExpanded = true;
|
private bool _tagsExpanded = true;
|
||||||
private bool _trackersExpanded = true;
|
private bool _trackersExpanded = true;
|
||||||
|
private readonly Dictionary<string, TrackerFilterItem> _trackerItems = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
protected string Status { get; set; } = Models.Status.All.ToString();
|
protected string Status { get; set; } = Models.Status.All.ToString();
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected Dictionary<string, int> Categories => GetCategories();
|
protected Dictionary<string, int> Categories => GetCategories();
|
||||||
|
|
||||||
protected Dictionary<string, int> Trackers => GetTrackers();
|
private IReadOnlyList<TrackerFilterItem> Trackers => GetTrackers();
|
||||||
|
|
||||||
protected Dictionary<string, int> Statuses => GetStatuses();
|
protected Dictionary<string, int> Statuses => GetStatuses();
|
||||||
|
|
||||||
@@ -88,6 +92,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
|
|
||||||
protected string? ContextMenuTracker { get; set; }
|
protected string? ContextMenuTracker { get; set; }
|
||||||
|
|
||||||
|
private bool CanRemoveTracker => ContextMenuTracker is not null
|
||||||
|
&& _trackerItems.TryGetValue(ContextMenuTracker, out var trackerItem)
|
||||||
|
&& !trackerItem.IsSynthetic
|
||||||
|
&& trackerItem.Urls.Count > 0;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
var status = await LocalStorage.GetItemAsStringAsync(_statusSelectionStorageKey);
|
var status = await LocalStorage.GetItemAsStringAsync(_statusSelectionStorageKey);
|
||||||
@@ -314,6 +323,32 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RemoveTracker()
|
||||||
|
{
|
||||||
|
if (ContextMenuTracker is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_trackerItems.TryGetValue(ContextMenuTracker, out var trackerItem) || trackerItem.IsSynthetic)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trackerItem.Urls.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hashes = GetAffectedTorrentHashes(_trackerType);
|
||||||
|
if (hashes.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ApiClient.RemoveTrackers(trackerItem.Urls, hashes: hashes.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task AddTag()
|
protected async Task AddTag()
|
||||||
{
|
{
|
||||||
if (ContextMenuTag is null)
|
if (ContextMenuTag is null)
|
||||||
@@ -352,25 +387,25 @@ 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)
|
||||||
{
|
{
|
||||||
var torrents = GetAffectedTorrentHashes(type);
|
var torrents = GetAffectedTorrentHashes(type);
|
||||||
|
|
||||||
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
|
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, [.. torrents]);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, int> GetTags()
|
private Dictionary<string, int> GetTags()
|
||||||
@@ -393,17 +428,59 @@ namespace Lantean.QBTMud.Components
|
|||||||
return MainData.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count);
|
return MainData.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, int> GetTrackers()
|
private IReadOnlyList<TrackerFilterItem> GetTrackers()
|
||||||
{
|
{
|
||||||
if (MainData is null)
|
if (MainData is null)
|
||||||
{
|
{
|
||||||
return [];
|
_trackerItems.Clear();
|
||||||
|
return Array.Empty<TrackerFilterItem>();
|
||||||
}
|
}
|
||||||
|
|
||||||
return MainData.TrackersState
|
var items = new List<TrackerFilterItem>();
|
||||||
.GroupBy(d => GetHostName(d.Key))
|
_trackerItems.Clear();
|
||||||
.Select(l => new KeyValuePair<string, int>(GetHostName(l.First().Key), l.Sum(i => i.Value.Count)))
|
|
||||||
.ToDictionary(d => d.Key, d => d.Value);
|
AppendSpecialTrackerItem(FilterHelper.TRACKER_ALL, items);
|
||||||
|
AppendSpecialTrackerItem(FilterHelper.TRACKER_TRACKERLESS, items);
|
||||||
|
AppendSpecialTrackerItem(FilterHelper.TRACKER_ERROR, items);
|
||||||
|
AppendSpecialTrackerItem(FilterHelper.TRACKER_WARNING, items);
|
||||||
|
AppendSpecialTrackerItem(FilterHelper.TRACKER_ANNOUNCE_ERROR, items);
|
||||||
|
|
||||||
|
if (MainData.Trackers.Count > 0)
|
||||||
|
{
|
||||||
|
var hostGroups = new Dictionary<string, TrackerHostGroup>(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (var (url, hashes) in MainData.Trackers)
|
||||||
|
{
|
||||||
|
var host = GetHostName(url);
|
||||||
|
if (!hostGroups.TryGetValue(host, out var group))
|
||||||
|
{
|
||||||
|
group = new TrackerHostGroup(host);
|
||||||
|
hostGroups[host] = group;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashes is not null)
|
||||||
|
{
|
||||||
|
foreach (var hash in hashes)
|
||||||
|
{
|
||||||
|
if (MainData.Torrents.ContainsKey(hash))
|
||||||
|
{
|
||||||
|
group.Hashes.Add(hash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
group.Urls.Add(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var group in hostGroups.Values.OrderBy(g => g.Host, StringComparer.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
var urls = group.Urls.Distinct(StringComparer.Ordinal).ToArray();
|
||||||
|
var item = new TrackerFilterItem(group.Host, group.Host, group.Hashes.Count, false, urls);
|
||||||
|
items.Add(item);
|
||||||
|
_trackerItems[item.Key] = item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
}
|
}
|
||||||
|
|
||||||
private Dictionary<string, int> GetStatuses()
|
private Dictionary<string, int> GetStatuses()
|
||||||
@@ -464,6 +541,37 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void AppendSpecialTrackerItem(string key, List<TrackerFilterItem> items)
|
||||||
|
{
|
||||||
|
if (MainData is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = MainData.TrackersState.TryGetValue(key, out var set) ? set.Count : 0;
|
||||||
|
var item = new TrackerFilterItem(key, key, count, true, Array.Empty<string>());
|
||||||
|
items.Add(item);
|
||||||
|
_trackerItems[key] = item;
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TrackerHostGroup
|
||||||
|
{
|
||||||
|
public TrackerHostGroup(string host)
|
||||||
|
{
|
||||||
|
Host = host;
|
||||||
|
Hashes = new HashSet<string>(StringComparer.Ordinal);
|
||||||
|
Urls = new List<string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Host { get; }
|
||||||
|
|
||||||
|
public HashSet<string> Hashes { get; }
|
||||||
|
|
||||||
|
public List<string> Urls { get; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record TrackerFilterItem(string Key, string DisplayName, int Count, bool IsSynthetic, IReadOnlyList<string> Urls);
|
||||||
|
|
||||||
private static string GetHostName(string tracker)
|
private static string GetHostName(string tracker)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -26,7 +26,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
|
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
|
||||||
|
|
||||||
@@ -68,6 +68,21 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|
||||||
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">Confirmation</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent Class="pt-0">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Confirm torrent recheck" Value="ConfirmTorrentRecheck" ValueChanged="ConfirmTorrentRecheckChanged" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
@@ -16,6 +16,8 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
protected int SaveResumeDataInterval { get; private set; }
|
protected int SaveResumeDataInterval { get; private set; }
|
||||||
protected int TorrentFileSizeLimit { get; private set; }
|
protected int TorrentFileSizeLimit { get; private set; }
|
||||||
protected bool RecheckCompletedTorrents { get; private set; }
|
protected bool RecheckCompletedTorrents { get; private set; }
|
||||||
|
|
||||||
|
protected bool ConfirmTorrentRecheck { get; private set; }
|
||||||
protected string? AppInstanceName { get; private set; }
|
protected string? AppInstanceName { get; private set; }
|
||||||
protected int RefreshInterval { get; private set; }
|
protected int RefreshInterval { get; private set; }
|
||||||
protected bool ResolvePeerCountries { get; private set; }
|
protected bool ResolvePeerCountries { get; private set; }
|
||||||
@@ -97,6 +99,7 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
|
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
|
||||||
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
|
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
|
||||||
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
|
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
|
||||||
|
ConfirmTorrentRecheck = Preferences.ConfirmTorrentRecheck;
|
||||||
AppInstanceName = Preferences.AppInstanceName;
|
AppInstanceName = Preferences.AppInstanceName;
|
||||||
RefreshInterval = Preferences.RefreshInterval;
|
RefreshInterval = Preferences.RefreshInterval;
|
||||||
ResolvePeerCountries = Preferences.ResolvePeerCountries;
|
ResolvePeerCountries = Preferences.ResolvePeerCountries;
|
||||||
@@ -209,6 +212,13 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task ConfirmTorrentRecheckChanged(bool value)
|
||||||
|
{
|
||||||
|
ConfirmTorrentRecheck = value;
|
||||||
|
UpdatePreferences.ConfirmTorrentRecheck = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task AppInstanceNameChanged(string value)
|
protected async Task AppInstanceNameChanged(string value)
|
||||||
{
|
{
|
||||||
AppInstanceName = value;
|
AppInstanceName = value;
|
||||||
@@ -17,6 +17,24 @@
|
|||||||
</MudCardContent>
|
</MudCardContent>
|
||||||
</MudCard>
|
</MudCard>
|
||||||
|
|
||||||
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
|
<MudCardHeader>
|
||||||
|
<CardHeaderContent>
|
||||||
|
<MudText Typo="Typo.subtitle2">Transfer List</MudText>
|
||||||
|
</CardHeaderContent>
|
||||||
|
</MudCardHeader>
|
||||||
|
<MudCardContent Class="pt-0">
|
||||||
|
<MudGrid>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Confirm when deleting torrents" Value="ConfirmTorrentDeletion" ValueChanged="ConfirmTorrentDeletionChanged" />
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12">
|
||||||
|
<FieldSwitch Label="Show external IP in status bar" Value="StatusBarExternalIp" ValueChanged="StatusBarExternalIpChanged" />
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
</MudCardContent>
|
||||||
|
</MudCard>
|
||||||
|
|
||||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||||
<MudCardHeader>
|
<MudCardHeader>
|
||||||
<CardHeaderContent>
|
<CardHeaderContent>
|
||||||
@@ -4,6 +4,10 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
{
|
{
|
||||||
public partial class BehaviourOptions : Options
|
public partial class BehaviourOptions : Options
|
||||||
{
|
{
|
||||||
|
protected bool ConfirmTorrentDeletion { get; set; }
|
||||||
|
|
||||||
|
protected bool StatusBarExternalIp { get; set; }
|
||||||
|
|
||||||
protected bool FileLogEnabled { get; set; }
|
protected bool FileLogEnabled { get; set; }
|
||||||
|
|
||||||
protected string? FileLogPath { get; set; }
|
protected string? FileLogPath { get; set; }
|
||||||
@@ -27,6 +31,8 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmTorrentDeletion = Preferences.ConfirmTorrentDeletion;
|
||||||
|
StatusBarExternalIp = Preferences.StatusBarExternalIp;
|
||||||
FileLogEnabled = Preferences.FileLogEnabled;
|
FileLogEnabled = Preferences.FileLogEnabled;
|
||||||
FileLogPath = Preferences.FileLogPath;
|
FileLogPath = Preferences.FileLogPath;
|
||||||
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
|
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
|
||||||
@@ -39,6 +45,20 @@ namespace Lantean.QBTMud.Components.Options
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected async Task ConfirmTorrentDeletionChanged(bool value)
|
||||||
|
{
|
||||||
|
ConfirmTorrentDeletion = value;
|
||||||
|
UpdatePreferences.ConfirmTorrentDeletion = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected async Task StatusBarExternalIpChanged(bool value)
|
||||||
|
{
|
||||||
|
StatusBarExternalIp = value;
|
||||||
|
UpdatePreferences.StatusBarExternalIp = value;
|
||||||
|
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||||
|
}
|
||||||
|
|
||||||
protected async Task FileLogEnabledChanged(bool value)
|
protected async Task FileLogEnabledChanged(bool value)
|
||||||
{
|
{
|
||||||
FileLogEnabled = value;
|
FileLogEnabled = 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">
|
||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ namespace Lantean.QBTMud.Components
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected IPeerDataManager PeerDataManager { get; set; } = default!;
|
||||||
|
|
||||||
protected PeerList? PeerList { get; set; }
|
protected PeerList? PeerList { get; set; }
|
||||||
|
|
||||||
@@ -78,11 +78,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
|
var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
|
||||||
if (PeerList is null || peers.FullUpdate)
|
if (PeerList is null || peers.FullUpdate)
|
||||||
{
|
{
|
||||||
PeerList = DataManager.CreatePeerList(peers);
|
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||||
}
|
}
|
||||||
_requestId = peers.RequestId;
|
_requestId = peers.RequestId;
|
||||||
|
|
||||||
@@ -200,11 +200,11 @@ namespace Lantean.QBTMud.Components
|
|||||||
}
|
}
|
||||||
if (PeerList is null || peers.FullUpdate)
|
if (PeerList is null || peers.FullUpdate)
|
||||||
{
|
{
|
||||||
PeerList = DataManager.CreatePeerList(peers);
|
PeerList = PeerDataManager.CreatePeerList(peers);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
DataManager.MergeTorrentPeers(peers, PeerList);
|
PeerDataManager.MergeTorrentPeers(peers, PeerList);
|
||||||
}
|
}
|
||||||
|
|
||||||
_requestId = peers.RequestId;
|
_requestId = peers.RequestId;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user