mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-11-01 20:43:36 +00:00
Compare commits
75 Commits
v1.1.0
...
feature/v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8eacd1ff78 | ||
|
|
08ed3e6a4c | ||
|
|
670cb3cc7d | ||
|
|
59fb1ee8e7 | ||
|
|
d482d58d3e | ||
|
|
233946d8f5 | ||
|
|
c268d11ba4 | ||
|
|
4bf5eaca0e | ||
|
|
22271ec308 | ||
|
|
a0ece0c0bb | ||
|
|
3eebdecffb | ||
|
|
24a048eb06 | ||
|
|
062da137a3 | ||
|
|
0fc3a5c39f | ||
|
|
7e79d237c7 | ||
|
|
960ca26ae3 | ||
|
|
25108a95c8 | ||
|
|
709d6a1a4e | ||
|
|
3ec8d189c6 | ||
|
|
11bc184cda | ||
|
|
78a357337b | ||
|
|
5c28ec3a23 | ||
|
|
6a14df189c | ||
|
|
fcb6dc436a | ||
|
|
21cd5e65be | ||
|
|
4666cb0b36 | ||
|
|
498420bf23 | ||
|
|
9d2ba7c57b | ||
|
|
2e843b1191 | ||
|
|
eb13b83548 | ||
|
|
e9daca6568 | ||
|
|
075ea9f855 | ||
|
|
d01204a703 | ||
|
|
ab1c594b07 | ||
|
|
6a5d8b2610 | ||
|
|
b8412bb232 | ||
|
|
e64a13c7c9 | ||
|
|
e4ea79a8ed | ||
|
|
0976b72411 | ||
|
|
965fbcd010 | ||
|
|
3d0dbde9f4 | ||
|
|
5b4fbde7b2 | ||
|
|
0db0ad4374 | ||
|
|
c390d83e4d | ||
|
|
8dd29c238d | ||
|
|
fca17edfd1 | ||
|
|
d8535fa262 | ||
|
|
1c6bfed6ee | ||
|
|
281caf8026 | ||
|
|
ff905e7cac | ||
|
|
cb80dd0d6b | ||
|
|
9113fb90ee | ||
|
|
d8b4e932d1 | ||
|
|
3d0d211d10 | ||
|
|
7db4f2f78d | ||
|
|
1f606b4449 | ||
|
|
88d66b4887 | ||
|
|
2ad7be1073 | ||
|
|
300e81345c | ||
|
|
9d8d84168e | ||
|
|
bb66b97f45 | ||
|
|
4824037ba7 | ||
|
|
1f9b631a36 | ||
|
|
2c744cd972 | ||
|
|
b02bb7cfae | ||
|
|
e4dac8556e | ||
|
|
a9a8a4eba8 | ||
|
|
bb524450f0 | ||
|
|
d4ac79af00 | ||
|
|
7370d73c59 | ||
|
|
8796cc0f24 | ||
|
|
b24ae440d4 | ||
|
|
bb90ce5216 | ||
|
|
4eaa46b2b3 | ||
|
|
1cf9f97187 |
@@ -1,4 +1,11 @@
|
||||
[*.cs]
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = crlf
|
||||
insert_final_newline = true
|
||||
|
||||
[*.cs]
|
||||
|
||||
# IDE0290: Use primary constructor
|
||||
csharp_style_prefer_primary_constructors = false
|
||||
@@ -77,3 +84,4 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
||||
tab_width = 4
|
||||
indent_size = 4
|
||||
end_of_line = crlf
|
||||
charset = utf-8
|
||||
78
.github/workflows/dotnet.yml
vendored
78
.github/workflows/dotnet.yml
vendored
@@ -4,6 +4,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
workflow_dispatch:
|
||||
pull_request:
|
||||
branches:
|
||||
- '**'
|
||||
@@ -32,9 +33,33 @@ jobs:
|
||||
id: gitversion
|
||||
run: |
|
||||
VERSION=$(dotnet gitversion /output json /showvariable FullSemVer)
|
||||
SAFE_VERSION=$(echo "$VERSION" | sed -E 's/[^A-Za-z0-9._+-]+/-/g')
|
||||
echo "VERSION=$VERSION" >> $GITHUB_ENV
|
||||
echo "VERSION_SAFE=$SAFE_VERSION" >> $GITHUB_ENV
|
||||
shell: bash
|
||||
|
||||
- name: Set Release Channel
|
||||
id: release_channel
|
||||
shell: bash
|
||||
run: |
|
||||
if [ "${GITHUB_REF}" = "refs/heads/master" ]; then
|
||||
echo "channel=stable" >> $GITHUB_OUTPUT
|
||||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
echo "label=Release" >> $GITHUB_OUTPUT
|
||||
elif [ "${GITHUB_REF}" = "refs/heads/develop" ]; then
|
||||
echo "channel=beta" >> $GITHUB_OUTPUT
|
||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||
echo "label=Beta" >> $GITHUB_OUTPUT
|
||||
elif [[ "${GITHUB_REF}" == refs/heads/feature/* ]]; then
|
||||
echo "channel=alpha" >> $GITHUB_OUTPUT
|
||||
echo "prerelease=true" >> $GITHUB_OUTPUT
|
||||
echo "label=Alpha" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "channel=none" >> $GITHUB_OUTPUT
|
||||
echo "prerelease=false" >> $GITHUB_OUTPUT
|
||||
echo "label=" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
- name: Restore dependencies
|
||||
run: dotnet restore
|
||||
|
||||
@@ -44,46 +69,63 @@ jobs:
|
||||
- name: Run Tests
|
||||
run: dotnet test --no-build --configuration Release
|
||||
|
||||
- name: Publish (only on master)
|
||||
if: github.ref == 'refs/heads/master'
|
||||
run: dotnet publish Lantean.QBTMud/Lantean.QBTMud.csproj -c Release -o output
|
||||
- name: Publish
|
||||
if: steps.release_channel.outputs.channel != 'none'
|
||||
run: dotnet publish src/Lantean.QBTMud/Lantean.QBTMud.csproj -c Release -o output
|
||||
|
||||
- name: Prepare Release ZIP
|
||||
if: github.ref == 'refs/heads/master'
|
||||
if: steps.release_channel.outputs.channel != 'none'
|
||||
run: |
|
||||
cd output
|
||||
mv wwwroot public
|
||||
zip -r "../qbt-mud-v${{ env.VERSION }}.zip" public
|
||||
zip -r "../qbt-mud-v${{ env.VERSION_SAFE }}.zip" public
|
||||
shell: bash
|
||||
|
||||
- name: Check if Tag Exists
|
||||
id: check_tag
|
||||
- name: Resolve Release Tag
|
||||
if: steps.release_channel.outputs.channel != 'none'
|
||||
id: resolve_tag
|
||||
shell: bash
|
||||
run: |
|
||||
if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then
|
||||
echo "TAG_EXISTS=true" >> $GITHUB_ENV
|
||||
if git rev-parse "${VERSION}" >/dev/null 2>&1; then
|
||||
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Using existing tag '${VERSION}'"
|
||||
elif git rev-parse "v${VERSION}" >/dev/null 2>&1; then
|
||||
echo "tag=v${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "exists=true" >> $GITHUB_OUTPUT
|
||||
echo "Using existing tag 'v${VERSION}'"
|
||||
else
|
||||
echo "TAG_EXISTS=false" >> $GITHUB_ENV
|
||||
echo "tag=${VERSION}" >> $GITHUB_OUTPUT
|
||||
echo "exists=false" >> $GITHUB_OUTPUT
|
||||
echo "::warning::No matching git tag found for '${VERSION}' or 'v${VERSION}'."
|
||||
fi
|
||||
|
||||
- name: Ensure Release Tag Exists
|
||||
if: steps.release_channel.outputs.channel == 'stable' && steps.resolve_tag.outputs.exists != 'true'
|
||||
shell: bash
|
||||
run: |
|
||||
echo "::error::Expected an existing git tag '${VERSION}' (or 'v${VERSION}') before creating a release."
|
||||
exit 1
|
||||
|
||||
- name: Create GitHub Release
|
||||
if: github.ref == 'refs/heads/master' && env.TAG_EXISTS == 'false'
|
||||
if: steps.release_channel.outputs.channel != 'none' && (steps.resolve_tag.outputs.exists == 'true' || steps.release_channel.outputs.channel != 'stable')
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
with:
|
||||
tag_name: v${{ env.VERSION }}
|
||||
release_name: Release v${{ env.VERSION }}
|
||||
draft: false
|
||||
prerelease: false
|
||||
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||
release_name: ${{ steps.release_channel.outputs.label }} ${{ steps.resolve_tag.outputs.tag }}
|
||||
draft: ${{ steps.release_channel.outputs.channel != 'alpha' }}
|
||||
prerelease: ${{ steps.release_channel.outputs.prerelease }}
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload Release Asset
|
||||
if: github.ref == 'refs/heads/master' && env.TAG_EXISTS == 'false'
|
||||
if: steps.release_channel.outputs.channel != 'none' && (steps.resolve_tag.outputs.exists == 'true' || steps.release_channel.outputs.channel != 'stable')
|
||||
uses: actions/upload-release-asset@v1
|
||||
with:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
asset_path: qbt-mud-v${{ env.VERSION }}.zip
|
||||
asset_name: qbt-mud-v${{ env.VERSION }}.zip
|
||||
asset_path: qbt-mud-v${{ env.VERSION_SAFE }}.zip
|
||||
asset_name: qbt-mud-v${{ env.VERSION_SAFE }}.zip
|
||||
asset_content_type: application/zip
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -360,4 +360,5 @@ MigrationBackup/
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
/output
|
||||
|
||||
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,3 +0,0 @@
|
||||
namespace Lantean.QBTMud.Test
|
||||
{
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
namespace Lantean.QBTMud.Test
|
||||
{
|
||||
internal class Tests
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using System.Linq.Expressions;
|
||||
using System.Text.Json;
|
||||
using Xunit.Abstractions;
|
||||
|
||||
namespace Lantean.QBTMud.Test
|
||||
{
|
||||
public class UnitTest1
|
||||
{
|
||||
private readonly ITestOutputHelper _testOutputHelper;
|
||||
|
||||
public UnitTest1(ITestOutputHelper testOutputHelper)
|
||||
{
|
||||
_testOutputHelper = testOutputHelper;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Test()
|
||||
{
|
||||
Test2(a => a.Name);
|
||||
}
|
||||
|
||||
private void Test2(Expression<Func<TestClass, object?>> expr)
|
||||
{
|
||||
var body = expr.Body;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Create()
|
||||
{
|
||||
var propInfo = typeof(TestClass).GetProperty("Name")!;
|
||||
|
||||
ParameterExpression expression = Expression.Parameter(typeof(TestClass), "a");
|
||||
var propertyExpression = Expression.Property(expression, "Value");
|
||||
|
||||
var convertExpression = Expression.Convert(propertyExpression, typeof(object));
|
||||
|
||||
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
|
||||
|
||||
Expression<Func<TestClass, object?>> expr2 = a => a.Name;
|
||||
|
||||
var x = l.Compile();
|
||||
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
|
||||
Assert.Equal(12, res);
|
||||
expr2.Compile();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ScanDir()
|
||||
{
|
||||
//Dictionary<string, string>
|
||||
var json = "{\r\n\t\"/this/is/path\": 1,\r\n\t\"/this/other\": 0,\r\n\t\"/home\": \"/path\"\r\n}";
|
||||
|
||||
var obj = JsonSerializer.Deserialize<Dictionary<string, SaveLocation>>(json, SerializerOptions.Options);
|
||||
}
|
||||
}
|
||||
|
||||
public class TestClass
|
||||
{
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string? Description { get; set; }
|
||||
|
||||
public long Value { get; set; }
|
||||
}
|
||||
}
|
||||
@@ -1,20 +1,33 @@
|
||||
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.8.34511.84
|
||||
# Visual Studio Version 18
|
||||
VisualStudioVersion = 18.0.11121.172
|
||||
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
|
||||
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
|
||||
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
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
.editorconfig = .editorconfig
|
||||
AGENTS.md = AGENTS.md
|
||||
readme.md = readme.md
|
||||
EndProjectSection
|
||||
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}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
test\AGENTS.md = test\AGENTS.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Source", "Source", "{2499085D-5140-4FBD-98AE-B281312FA6D6}"
|
||||
ProjectSection(SolutionItems) = preProject
|
||||
src\AGENTS.md = src\AGENTS.md
|
||||
EndProjectSection
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -33,10 +46,20 @@ Global
|
||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{715E075C-1D86-4A7F-BC72-E1E24A294F17} = {344EAF42-5D2B-4F56-8B28-1F3158A37E0A}
|
||||
{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7} = {2499085D-5140-4FBD-98AE-B281312FA6D6}
|
||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098} = {2499085D-5140-4FBD-98AE-B281312FA6D6}
|
||||
{796E865C-7AA6-4BD9-B12F-394801199A75} = {344EAF42-5D2B-4F56-8B28-1F3158A37E0A}
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
|
||||
EndGlobalSection
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
@if (IsMenu)
|
||||
{
|
||||
@foreach (var action in Actions)
|
||||
{
|
||||
if (action.SeparatorBefore)
|
||||
{
|
||||
<MudDivider />
|
||||
}
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="@action.Color" Href="@action.Href">@action.Text</MudMenuItem>
|
||||
}
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Undo" OnClick="ResetWebUI">Reset Web UI</MudMenuItem>
|
||||
<MudDivider />
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Logout" OnClick="Logout">Logout</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" OnClick="Exit">Exit qBittorrent</MudMenuItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavLink Icon="@Icons.Material.Outlined.Navigation" OnClick="NavigateBack">Torrents</MudNavLink>
|
||||
<MudDivider />
|
||||
@foreach (var action in Actions)
|
||||
{
|
||||
if (action.SeparatorBefore)
|
||||
{
|
||||
<MudDivider />
|
||||
}
|
||||
<MudNavLink Icon="@action.Icon" IconColor="@action.Color" Href="@action.Href">@action.Text</MudNavLink>
|
||||
}
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Components
|
||||
{
|
||||
public partial class ApplicationActions
|
||||
{
|
||||
private List<UIAction>? _actions;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public bool IsMenu { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Preferences? Preferences { get; set; }
|
||||
|
||||
protected IEnumerable<UIAction> Actions => GetActions();
|
||||
|
||||
private IEnumerable<UIAction> GetActions()
|
||||
{
|
||||
if (_actions is not null)
|
||||
{
|
||||
foreach (var action in _actions)
|
||||
{
|
||||
if (action.Name != "rss" || Preferences is not null && Preferences.RssProcessingEnabled)
|
||||
{
|
||||
yield return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_actions =
|
||||
[
|
||||
new("statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"),
|
||||
new("search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"),
|
||||
new("rss", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"),
|
||||
new("log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"),
|
||||
new("blocks", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"),
|
||||
new("tags", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true),
|
||||
new("categories", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"),
|
||||
new("settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true),
|
||||
new("about", "About", Icons.Material.Filled.Info, Color.Default, "/about"),
|
||||
];
|
||||
}
|
||||
|
||||
protected void NavigateBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
protected async Task ResetWebUI()
|
||||
{
|
||||
var preferences = new UpdatePreferences
|
||||
{
|
||||
AlternativeWebuiEnabled = false,
|
||||
};
|
||||
|
||||
await ApiClient.SetApplicationPreferences(preferences);
|
||||
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
|
||||
protected async Task Logout()
|
||||
{
|
||||
await DialogService.ShowConfirmDialog("Logout?", "Are you sure you want to logout?", async () =>
|
||||
{
|
||||
await ApiClient.Logout();
|
||||
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
});
|
||||
}
|
||||
|
||||
protected async Task Exit()
|
||||
{
|
||||
await DialogService.ShowConfirmDialog("Quit?", "Are you sure you want to exit qBittorrent?", ApiClient.Shutdown);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent" MaximumFileCount="50" >
|
||||
<ActivatorContent>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.CloudUpload">
|
||||
Choose files
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption="true" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Close</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Submit">Upload Torrents</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -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,82 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
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,93 +0,0 @@
|
||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
|
||||
</ContextMenu>
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
|
||||
<MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
|
||||
<MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
|
||||
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
|
||||
</MudMenu>
|
||||
<MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
|
||||
<MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
|
||||
<MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
|
||||
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
|
||||
</MudMenu>
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
|
||||
<MudSpacer />
|
||||
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
<DynamicTable
|
||||
@ref="Table"
|
||||
T="ContentItem"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Files"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="true"
|
||||
PreSorted="true"
|
||||
SelectedItemChanged="SelectedItemChanged"
|
||||
SortColumnChanged="SortColumnChanged"
|
||||
SortDirectionChanged="SortDirectionChanged"
|
||||
OnTableDataContextMenu="TableDataContextMenu"
|
||||
OnTableDataLongPress="TableDataLongPress"
|
||||
Class="file-list"
|
||||
/>
|
||||
|
||||
@code {
|
||||
private RenderFragment<RowContext<ContentItem>> NameColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
<div style="@($"margin-left: {(context.Data.Level * 14) + (context.Data.Level >= 1 ? 16 : 0)}px")">
|
||||
@if (context.Data.IsFolder)
|
||||
{
|
||||
<MudIconButton Class="folder-button" Edge="Edge.Start" ButtonType="ButtonType.Button" Icon="@(ExpandedNodes.Contains(context.Data.Name) ? Icons.Material.Filled.KeyboardArrowDown : Icons.Material.Filled.KeyboardArrowRight)" OnClick="@(c => ToggleNode(context.Data))"></MudIconButton>
|
||||
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-0" Style="margin-right: 4px; position: relative; top: 7px; margin-left: -15px" />
|
||||
}
|
||||
@context.Data.DisplayName
|
||||
</div>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private RenderFragment<RowContext<ContentItem>> PriorityColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
<MudSelect T="Priority" Dense="true" Value="@context.Data.Priority" ValueChanged="@(priority => PriorityValueChanged(context.Data, priority))" Class="mt-0">
|
||||
<MudSelectItem T="Priority" Value="Priority.DoNotDownload">Do not download</MudSelectItem>
|
||||
<MudSelectItem T="Priority" Value="Priority.Normal">Normal</MudSelectItem>
|
||||
<MudSelectItem T="Priority" Value="Priority.High">High</MudSelectItem>
|
||||
<MudSelectItem T="Priority" Value="Priority.Maximum">Maximum</MudSelectItem>
|
||||
</MudSelect>
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderFragment<RowContext<ContentItem>> ProgressBarColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
var value = (float?)context.GetValue();
|
||||
var color = value < 1 ? Color.Success : Color.Info;
|
||||
<MudProgressLinear title="Progress" Color="@color" Value="@((value ?? 0) * 100)" Class="progress-expand" Size="Size.Large">
|
||||
@DisplayHelpers.Percentage(value)
|
||||
</MudProgressLinear>;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
|
||||
@if (ContextMenuItem is not null)
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
|
||||
}
|
||||
</ContextMenu>
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||
</MudToolBar>
|
||||
|
||||
<DynamicTable T="Peer"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Peers"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="true"
|
||||
OnTableDataLongPress="TableDataLongPress"
|
||||
OnTableDataContextMenu="TableDataContextMenu"
|
||||
SelectedItemChanged="SelectedItemChanged"
|
||||
Class="details-list" />
|
||||
@@ -1 +0,0 @@
|
||||
<div id="progress"></div>
|
||||
@@ -1,32 +0,0 @@
|
||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
|
||||
@if (ContextMenuItem is not null)
|
||||
{
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Edit" IconColor="Color.Info" OnClick="EditTrackerToolbar">Edit tracker URL</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
|
||||
}
|
||||
</ContextMenu>
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||
</MudToolBar>
|
||||
|
||||
<DynamicTable @ref="Table"
|
||||
T="Lantean.QBitTorrentClient.Models.TorrentTracker"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Trackers"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
PreSorted="true"
|
||||
SortDirectionChanged="SortDirectionChanged"
|
||||
SortColumnChanged="SortColumnChanged"
|
||||
OnTableDataLongPress="TableDataLongPress"
|
||||
OnTableDataContextMenu="TableDataContextMenu"
|
||||
SelectedItemChanged="SelectedItemChanged"
|
||||
Class="file-list" />
|
||||
@@ -1,26 +0,0 @@
|
||||
@inherits MudComponentBase
|
||||
|
||||
<MudMenu @ref="FakeMenu" Style="display: none" OpenChanged="FakeOpenChanged"></MudMenu>
|
||||
|
||||
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
|
||||
<MudPopover tracker="@Id"
|
||||
Open="@_open"
|
||||
Class="unselectable"
|
||||
MaxHeight="@MaxHeight"
|
||||
AnchorOrigin="@AnchorOrigin"
|
||||
TransformOrigin="@TransformOrigin"
|
||||
RelativeWidth="@RelativeWidth"
|
||||
OverflowBehavior="OverflowBehavior.FlipAlways"
|
||||
Style="@_popoverStyle"
|
||||
@ontouchend:preventDefault>
|
||||
<CascadingValue Value="@(FakeMenu)">
|
||||
@if (_showChildren)
|
||||
{
|
||||
<MudList T="object" Class="unselectable" Dense="@Dense">
|
||||
@ChildContent
|
||||
</MudList>
|
||||
}
|
||||
</CascadingValue>
|
||||
</MudPopover>
|
||||
|
||||
<MudOverlay Visible="@(_open)" LockScroll="@LockScroll" AutoClose="true" OnClosed="@CloseMenuAsync" />
|
||||
@@ -1,290 +0,0 @@
|
||||
using Lantean.QBTMud.Interop;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
using MudBlazor.Utilities;
|
||||
|
||||
namespace Lantean.QBTMud.Components.UI
|
||||
{
|
||||
public partial class ContextMenu : MudComponentBase
|
||||
{
|
||||
private bool _open;
|
||||
private bool _showChildren;
|
||||
private string? _popoverStyle;
|
||||
private string? _id;
|
||||
|
||||
private double _x;
|
||||
private double _y;
|
||||
private bool _isResized = false;
|
||||
|
||||
private const double _diff = 64;
|
||||
|
||||
private string Id
|
||||
{
|
||||
get
|
||||
{
|
||||
_id ??= Guid.NewGuid().ToString();
|
||||
|
||||
return _id;
|
||||
}
|
||||
}
|
||||
|
||||
[Inject]
|
||||
public IJSRuntime JSRuntime { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
public IPopoverService PopoverService { get; set; } = default!;
|
||||
|
||||
/// <summary>
|
||||
/// If true, compact vertical padding will be applied to all menu items.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public bool Dense { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set to true if you want to prevent page from scrolling when the menu is open
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public bool LockScroll { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If true, the list menu will be same width as the parent.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public DropdownWidth RelativeWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the max height the menu can have when open.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public int? MaxHeight { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set the anchor origin point to determine where the popover will open from.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public Origin AnchorOrigin { get; set; } = Origin.TopLeft;
|
||||
|
||||
/// <summary>
|
||||
/// Sets the transform origin point for the popover.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public Origin TransformOrigin { get; set; } = Origin.TopLeft;
|
||||
|
||||
/// <summary>
|
||||
/// If true, menu will be disabled.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.Behavior)]
|
||||
public bool Disabled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.Appearance)]
|
||||
public bool Ripple { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the component has a drop-shadow. Default is true
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.Appearance)]
|
||||
public bool DropShadow { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Add menu items here
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupBehavior)]
|
||||
public RenderFragment? ChildContent { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Fired when the menu <see cref="Open"/> property changes.
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupBehavior)]
|
||||
public EventCallback<bool> OpenChanged { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int AdjustmentX { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public int AdjustmentY { get; set; }
|
||||
|
||||
protected MudMenu? FakeMenu { get; set; }
|
||||
|
||||
protected void FakeOpenChanged(bool value)
|
||||
{
|
||||
if (!value)
|
||||
{
|
||||
_open = false;
|
||||
}
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Opens the menu.
|
||||
/// </summary>
|
||||
/// <param name="args">
|
||||
/// The arguments of the calling mouse/pointer event.
|
||||
/// </param>
|
||||
public async Task OpenMenuAsync(EventArgs args)
|
||||
{
|
||||
if (Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// long press on iOS triggers selection, so clear it
|
||||
await JSRuntime.ClearSelection();
|
||||
|
||||
if (args is not LongPressEventArgs)
|
||||
{
|
||||
_showChildren = true;
|
||||
}
|
||||
|
||||
_open = true;
|
||||
_isResized = false;
|
||||
StateHasChanged();
|
||||
|
||||
var (x, y) = GetPositionFromArgs(args);
|
||||
_x = x;
|
||||
_y = y;
|
||||
|
||||
SetPopoverStyle(x, y);
|
||||
|
||||
StateHasChanged();
|
||||
|
||||
await OpenChanged.InvokeAsync(_open);
|
||||
|
||||
// long press on iOS triggers selection, so clear it
|
||||
await JSRuntime.ClearSelection();
|
||||
|
||||
if (args is LongPressEventArgs)
|
||||
{
|
||||
await Task.Delay(1000);
|
||||
_showChildren = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Closes the menu.
|
||||
/// </summary>
|
||||
public Task CloseMenuAsync()
|
||||
{
|
||||
_open = false;
|
||||
_popoverStyle = null;
|
||||
StateHasChanged();
|
||||
|
||||
return OpenChanged.InvokeAsync(_open);
|
||||
}
|
||||
|
||||
private void SetPopoverStyle(double x, double y)
|
||||
{
|
||||
_popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggle the visibility of the menu.
|
||||
/// </summary>
|
||||
public async Task ToggleMenuAsync(EventArgs args)
|
||||
{
|
||||
if (Disabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_open)
|
||||
{
|
||||
await CloseMenuAsync();
|
||||
}
|
||||
else
|
||||
{
|
||||
await OpenMenuAsync(args);
|
||||
}
|
||||
}
|
||||
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_isResized)
|
||||
{
|
||||
//await DeterminePosition();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
//private async Task DeterminePosition()
|
||||
//{
|
||||
// var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
|
||||
// double? contextMenuHeight = null;
|
||||
// double? contextMenuWidth = null;
|
||||
|
||||
// var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
|
||||
|
||||
// var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
|
||||
// if (popoverSize.Height > 0)
|
||||
// {
|
||||
// contextMenuHeight = popoverSize.Height;
|
||||
// contextMenuWidth = popoverSize.Width;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // the bottom position of the popover will be rendered off screen
|
||||
// if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
|
||||
// {
|
||||
// // adjust the top of the context menu
|
||||
// var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
|
||||
// _y -= overshoot;
|
||||
|
||||
// if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
|
||||
// {
|
||||
// MaxHeight = (int)(mainContentSize.Height - _y + _diff);
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (_x + contextMenuWidth.Value > mainContentSize.Width)
|
||||
// {
|
||||
// var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
|
||||
// _x -= overshoot;
|
||||
// }
|
||||
|
||||
// SetPopoverStyle(_x, _y);
|
||||
// _isResized = true;
|
||||
// await InvokeAsync(StateHasChanged);
|
||||
//}
|
||||
|
||||
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
|
||||
{
|
||||
double x, y;
|
||||
if (eventArgs is MouseEventArgs mouseEventArgs)
|
||||
{
|
||||
x = mouseEventArgs.ClientX;
|
||||
y = mouseEventArgs.ClientY;
|
||||
}
|
||||
else if (eventArgs is LongPressEventArgs longPressEventArgs)
|
||||
{
|
||||
x = longPressEventArgs.ClientX;
|
||||
y = longPressEventArgs.ClientY;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException("Invalid eventArgs type.");
|
||||
}
|
||||
|
||||
return (x + AdjustmentX, y + AdjustmentY);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
@ChildContent
|
||||
@@ -1,6 +0,0 @@
|
||||
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="WebSeeds"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
Class="details-list" />
|
||||
@@ -1,439 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Components.Dialogs;
|
||||
using Lantean.QBTMud.Filter;
|
||||
using Lantean.QBTMud.Models;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
public static class DialogHelper
|
||||
{
|
||||
public const long _maxFileSize = 4194304;
|
||||
public static readonly DialogOptions ConfirmDialogOptions = new() { BackgroundClass = "background-blur", MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, BackgroundClass = "background-blur", FullWidth = true };
|
||||
|
||||
public static readonly DialogOptions FullScreenDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.ExtraExtraLarge, BackgroundClass = "background-blur", FullWidth = true };
|
||||
public static readonly DialogOptions NonBlurConfirmDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
|
||||
public static readonly DialogOptions NonBlurFormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
|
||||
|
||||
public static async Task<string?> InvokeAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient)
|
||||
{
|
||||
var reference = await dialogService.ShowAsync<CategoryPropertiesDialog>("Add Category", NonBlurFormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var category = (Category)dialogResult.Data;
|
||||
|
||||
await apiClient.AddCategory(category.Name, category.SavePath);
|
||||
|
||||
return category.Name;
|
||||
}
|
||||
|
||||
public static async Task InvokeAddTorrentFileDialog(this IDialogService dialogService, IApiClient apiClient)
|
||||
{
|
||||
var result = await dialogService.ShowAsync<AddTorrentFileDialog>("Upload local torrent", FormDialogOptions);
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = (AddTorrentFileOptions)dialogResult.Data;
|
||||
|
||||
var streams = new List<Stream>();
|
||||
|
||||
var files = new Dictionary<string, Stream>();
|
||||
foreach (var file in options.Files)
|
||||
{
|
||||
var stream = file.OpenReadStream(_maxFileSize);
|
||||
streams.Add(stream);
|
||||
files.Add(file.Name, stream);
|
||||
}
|
||||
|
||||
var addTorrentParams = CreateAddTorrentParams(options);
|
||||
addTorrentParams.Torrents = files;
|
||||
|
||||
await apiClient.AddTorrent(addTorrentParams);
|
||||
|
||||
foreach (var stream in streams)
|
||||
{
|
||||
await stream.DisposeAsync();
|
||||
}
|
||||
}
|
||||
|
||||
private static QBitTorrentClient.Models.AddTorrentParams CreateAddTorrentParams(TorrentOptions options)
|
||||
{
|
||||
var addTorrentParams = new QBitTorrentClient.Models.AddTorrentParams();
|
||||
addTorrentParams.AddToTopOfQueue = options.AddToTopOfQueue;
|
||||
addTorrentParams.AutoTorrentManagement = options.TorrentManagementMode;
|
||||
addTorrentParams.Category = options.Category;
|
||||
if (!string.IsNullOrEmpty(options.ContentLayout))
|
||||
{
|
||||
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
|
||||
}
|
||||
if (string.IsNullOrEmpty(options.Cookie))
|
||||
{
|
||||
addTorrentParams.Cookie = options.Cookie;
|
||||
}
|
||||
addTorrentParams.DownloadLimit = options.DownloadLimit;
|
||||
addTorrentParams.DownloadPath = options.DownloadPath;
|
||||
addTorrentParams.FirstLastPiecePriority = options.DownloadFirstAndLastPiecesFirst;
|
||||
addTorrentParams.InactiveSeedingTimeLimit = options.InactiveSeedingTimeLimit;
|
||||
addTorrentParams.Paused = !options.StartTorrent;
|
||||
addTorrentParams.RatioLimit = options.RatioLimit;
|
||||
addTorrentParams.RenameTorrent = options.RenameTorrent;
|
||||
addTorrentParams.SavePath = options.SavePath;
|
||||
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
|
||||
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
|
||||
if (!string.IsNullOrEmpty(options.ShareLimitAction))
|
||||
{
|
||||
addTorrentParams.ShareLimitAction = Enum.Parse<QBitTorrentClient.Models.ShareLimitAction>(options.ShareLimitAction);
|
||||
}
|
||||
addTorrentParams.SkipChecking = options.SkipHashCheck;
|
||||
if (!string.IsNullOrEmpty(options.StopCondition))
|
||||
{
|
||||
addTorrentParams.StopCondition = Enum.Parse<QBitTorrentClient.Models.StopCondition>(options.StopCondition);
|
||||
}
|
||||
addTorrentParams.Stopped = !options.StartTorrent;
|
||||
addTorrentParams.Tags = options.Tags;
|
||||
addTorrentParams.UploadLimit = options.UploadLimit;
|
||||
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
|
||||
return addTorrentParams;
|
||||
}
|
||||
|
||||
public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient, string? url = null)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(AddTorrentLinkDialog.Url), url }
|
||||
};
|
||||
|
||||
var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", parameters, FormDialogOptions);
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var options = (AddTorrentLinkOptions)dialogResult.Data;
|
||||
|
||||
var addTorrentParams = CreateAddTorrentParams(options);
|
||||
addTorrentParams.Urls = options.Urls;
|
||||
|
||||
await apiClient.AddTorrent(addTorrentParams);
|
||||
}
|
||||
|
||||
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
|
||||
{
|
||||
if (hashes.Length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(DeleteDialog.Count), hashes.Length }
|
||||
};
|
||||
|
||||
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
await apiClient.DeleteTorrents(hashes, (bool)dialogResult.Data);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static async Task InvokeDownloadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
|
||||
{
|
||||
Func<long, string> valueDisplayFunc = v => v == Limits.NoLimit ? "∞" : v.ToString();
|
||||
Func<string, long> valueGetFunc = v => v == "∞" ? Limits.NoLimit : long.Parse(v);
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(SliderFieldDialog<long>.Min), -1L },
|
||||
{ nameof(SliderFieldDialog<long>.Max), 1000L },
|
||||
{ nameof(SliderFieldDialog<long>.Value), rate / 1024 },
|
||||
{ nameof(SliderFieldDialog<long>.ValueDisplayFunc), valueDisplayFunc },
|
||||
{ nameof(SliderFieldDialog<long>.ValueGetFunc), valueGetFunc },
|
||||
{ nameof(SliderFieldDialog<long>.Label), "Download rate limit" },
|
||||
{ nameof(SliderFieldDialog<long>.Adornment), Adornment.End },
|
||||
{ nameof(SliderFieldDialog<long>.AdornmentText), "KiB/s" },
|
||||
};
|
||||
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Download Rate", parameters, FormDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var kibs = (long)dialogResult.Data;
|
||||
await apiClient.SetTorrentDownloadLimit(kibs * 1024, null, hashes.ToArray());
|
||||
}
|
||||
|
||||
public static async Task<string?> InvokeEditCategoryDialog(this IDialogService dialogService, IApiClient apiClient, string categoryName)
|
||||
{
|
||||
var category = (await apiClient.GetAllCategories()).FirstOrDefault(c => c.Key == categoryName).Value;
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(CategoryPropertiesDialog.Category), category?.Name },
|
||||
{ nameof(CategoryPropertiesDialog.SavePath), category?.SavePath },
|
||||
};
|
||||
|
||||
var reference = await dialogService.ShowAsync<CategoryPropertiesDialog>("Edit Category", parameters, NonBlurFormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var updatedCategory = (Category)dialogResult.Data;
|
||||
|
||||
await apiClient.EditCategory(updatedCategory.Name, updatedCategory.SavePath);
|
||||
|
||||
return updatedCategory.Name;
|
||||
}
|
||||
|
||||
public static async Task InvokeRenameFilesDialog(this IDialogService dialogService, string hash)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(RenameFilesDialog.Hash), hash }
|
||||
};
|
||||
|
||||
await dialogService.ShowAsync<RenameFilesDialog>("Rename Files", parameters, FullScreenDialogOptions);
|
||||
}
|
||||
|
||||
public static async Task InvokeRssRulesDialog(this IDialogService dialogService)
|
||||
{
|
||||
await dialogService.ShowAsync<RssRulesDialog>("Edit Rss Auto Downloading Rules", FullScreenDialogOptions);
|
||||
}
|
||||
|
||||
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<Torrent> torrents)
|
||||
{
|
||||
var torrentShareRatios = torrents.Select(t => new ShareRatioMax
|
||||
{
|
||||
InactiveSeedingTimeLimit = t.InactiveSeedingTimeLimit,
|
||||
MaxInactiveSeedingTime = t.InactiveSeedingTimeLimit,
|
||||
MaxRatio = t.MaxRatio,
|
||||
MaxSeedingTime = t.MaxSeedingTime,
|
||||
RatioLimit = t.RatioLimit,
|
||||
SeedingTimeLimit = t.SeedingTimeLimit,
|
||||
});
|
||||
|
||||
var torrentsHaveSameShareRatio = torrentShareRatios.Distinct().Count() == 1;
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? torrentShareRatios.FirstOrDefault() : null },
|
||||
};
|
||||
var result = await dialogService.ShowAsync<ShareRatioDialog>("Share ratio", parameters, FormDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var shareRatio = (ShareRatio)dialogResult.Data;
|
||||
|
||||
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, null, torrents.Select(t => t.Hash).ToArray());
|
||||
}
|
||||
|
||||
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
|
||||
{
|
||||
var result = await dialogService.ShowStringFieldDialog(title, label, value);
|
||||
|
||||
if (result is not null)
|
||||
{
|
||||
await onSuccess(result);
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task InvokeUploadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
|
||||
{
|
||||
Func<long, string> valueDisplayFunc = v => v == Limits.NoLimit ? "∞" : v.ToString();
|
||||
Func<string, long> valueGetFunc = v => v == "∞" ? Limits.NoLimit : long.Parse(v);
|
||||
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(SliderFieldDialog<long>.Min), -1L },
|
||||
{ nameof(SliderFieldDialog<long>.Max), 1000L },
|
||||
{ nameof(SliderFieldDialog<long>.Value), rate / 1024 },
|
||||
{ nameof(SliderFieldDialog<long>.ValueDisplayFunc), valueDisplayFunc },
|
||||
{ nameof(SliderFieldDialog<long>.ValueGetFunc), valueGetFunc },
|
||||
{ nameof(SliderFieldDialog<long>.Label), "Upload rate limit" },
|
||||
{ nameof(SliderFieldDialog<long>.Adornment), Adornment.End },
|
||||
{ nameof(SliderFieldDialog<long>.AdornmentText), "KiB/s" },
|
||||
};
|
||||
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var kibs = (long)dialogResult.Data;
|
||||
await apiClient.SetTorrentUploadLimit(kibs * 1024, null, hashes.ToArray());
|
||||
}
|
||||
|
||||
public static async Task<HashSet<QBitTorrentClient.Models.PeerId>?> ShowAddPeersDialog(this IDialogService dialogService)
|
||||
{
|
||||
var reference = await dialogService.ShowAsync<AddPeerDialog>("Add Peer", NonBlurFormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var peers = (HashSet<QBitTorrentClient.Models.PeerId>)dialogResult.Data;
|
||||
|
||||
return peers;
|
||||
}
|
||||
|
||||
public static async Task<HashSet<string>?> ShowAddTagsDialog(this IDialogService dialogService)
|
||||
{
|
||||
var reference = await dialogService.ShowAsync<AddTagDialog>("Add Tags", NonBlurFormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tags = (HashSet<string>)dialogResult.Data;
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
public static async Task<HashSet<string>?> ShowAddTrackersDialog(this IDialogService dialogService)
|
||||
{
|
||||
var reference = await dialogService.ShowAsync<AddTrackerDialog>("Add Tracker", NonBlurFormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var tags = (HashSet<string>)dialogResult.Data;
|
||||
|
||||
return tags;
|
||||
}
|
||||
|
||||
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths, Dictionary<string, int> ColumnOrder)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths, Dictionary<string, int> order)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
|
||||
{ nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
|
||||
{ nameof(ColumnOptionsDialog<T>.Widths), widths },
|
||||
{ nameof(ColumnOptionsDialog<T>.Order), order },
|
||||
};
|
||||
|
||||
var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
|
||||
var dialogResult = await reference.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return ((HashSet<string>, Dictionary<string, int?>, Dictionary<string, int>))dialogResult.Data;
|
||||
}
|
||||
|
||||
public static async Task<bool> ShowConfirmDialog(this IDialogService dialogService, string title, string content)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(ConfirmDialog.Content), content }
|
||||
};
|
||||
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
return dialogResult is not null && !dialogResult.Canceled;
|
||||
}
|
||||
|
||||
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func<Task> onSuccess)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(ConfirmDialog.Content), content }
|
||||
};
|
||||
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await onSuccess();
|
||||
}
|
||||
|
||||
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Action onSuccess)
|
||||
{
|
||||
await dialogService.ShowConfirmDialog(title, content, () =>
|
||||
{
|
||||
onSuccess();
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
}
|
||||
|
||||
public static async Task<List<PropertyFilterDefinition<T>>?> ShowFilterOptionsDialog<T>(this IDialogService dialogService, List<PropertyFilterDefinition<T>>? propertyFilterDefinitions)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(FilterOptionsDialog<T>.FilterDefinitions), propertyFilterDefinitions },
|
||||
};
|
||||
|
||||
var result = await dialogService.ShowAsync<FilterOptionsDialog<T>>("Filters", parameters, FormDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (List<PropertyFilterDefinition<T>>?)dialogResult.Data;
|
||||
}
|
||||
|
||||
public static async Task<string?> ShowStringFieldDialog(this IDialogService dialogService, string title, string label, string? value)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(StringFieldDialog.Label), label },
|
||||
{ nameof(StringFieldDialog.Value), value }
|
||||
};
|
||||
var result = await dialogService.ShowAsync<StringFieldDialog>(title, parameters, FormDialogOptions);
|
||||
|
||||
var dialogResult = await result.Result;
|
||||
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return (string)dialogResult.Data;
|
||||
}
|
||||
|
||||
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, UIAction parent, Dictionary<string, Torrent> torrents, QBitTorrentClient.Models.Preferences? preferences)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(SubMenuDialog.ParentAction), parent },
|
||||
{ nameof(SubMenuDialog.Hashes), hashes },
|
||||
{ nameof(SubMenuDialog.Torrents), torrents },
|
||||
{ nameof(SubMenuDialog.Preferences), preferences },
|
||||
};
|
||||
|
||||
await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
|
||||
namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
internal static class VersionHelper
|
||||
{
|
||||
private static int? _version;
|
||||
|
||||
private const int _defaultVersion = 5;
|
||||
|
||||
public static int DefaultVersion => _defaultVersion;
|
||||
|
||||
public static int GetMajorVersion(string? version)
|
||||
{
|
||||
if (_version is not null)
|
||||
{
|
||||
return _version.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(version))
|
||||
{
|
||||
return _defaultVersion;
|
||||
}
|
||||
|
||||
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
|
||||
{
|
||||
return _defaultVersion;
|
||||
}
|
||||
|
||||
_version = theVersion.Major;
|
||||
|
||||
return _version.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
<PackageReference Include="MudBlazor" Version="8.7.0" />
|
||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,9 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@layout LoggedInLayout
|
||||
|
||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
||||
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
@Body
|
||||
</MudMainContent>
|
||||
@@ -1,11 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@layout LoggedInLayout
|
||||
|
||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
||||
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
||||
@Body
|
||||
</CascadingValue>
|
||||
</MudMainContent>
|
||||
@@ -1,69 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@layout MainLayout
|
||||
|
||||
<PageTitle>qBittorrent @Version Web UI</PageTitle>
|
||||
|
||||
@if (!IsAuthenticated)
|
||||
{
|
||||
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-7" Style="width: 100%; height: 30px" />
|
||||
return;
|
||||
}
|
||||
|
||||
<CascadingValue Value="Torrents">
|
||||
<CascadingValue Value="MainData">
|
||||
<CascadingValue Value="Preferences">
|
||||
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
|
||||
<CascadingValue Value="SortColumn" Name="SortColumn">
|
||||
<CascadingValue Value="SortDirectionChanged" Name="SortDirectionChanged">
|
||||
<CascadingValue Value="SortDirection" Name="SortDirection">
|
||||
<CascadingValue Value="CategoryChanged" Name="CategoryChanged">
|
||||
<CascadingValue Value="StatusChanged" Name="StatusChanged">
|
||||
<CascadingValue Value="TagChanged" Name="TagChanged">
|
||||
<CascadingValue Value="TrackerChanged" Name="TrackerChanged">
|
||||
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
|
||||
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
|
||||
<CascadingValue Value="Version" Name="Version">
|
||||
@Body
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
|
||||
@if (MainData?.LostConnection == true)
|
||||
{
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
||||
}
|
||||
<MudSpacer />
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||
@{
|
||||
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
||||
}
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
|
||||
<MudText Class="mr-1 mb-1">
|
||||
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
|
||||
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
|
||||
</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Info" />
|
||||
<MudText Class="mr-1 mb-1">
|
||||
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
|
||||
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
|
||||
</MudText>
|
||||
</MudAppBar>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
@@ -1,191 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Components;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Layout
|
||||
{
|
||||
public partial class LoggedInLayout : IDisposable
|
||||
{
|
||||
private readonly bool _refreshEnabled = true;
|
||||
|
||||
private int _requestId = 0;
|
||||
private bool _disposedValue;
|
||||
private readonly CancellationTokenSource _timerCancellationToken = new();
|
||||
private int _refreshInterval = 1500;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[CascadingParameter(Name = "DrawerOpen")]
|
||||
public bool DrawerOpen { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public Menu? Menu { get; set; }
|
||||
|
||||
protected MainData? MainData { get; set; }
|
||||
|
||||
protected string Category { get; set; } = FilterHelper.CATEGORY_ALL;
|
||||
|
||||
protected string Tag { get; set; } = FilterHelper.TAG_ALL;
|
||||
|
||||
protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL;
|
||||
|
||||
protected Status Status { get; set; } = Status.All;
|
||||
|
||||
protected QBitTorrentClient.Models.Preferences? Preferences { get; set; }
|
||||
|
||||
protected string? SortColumn { get; set; }
|
||||
|
||||
protected SortDirection SortDirection { get; set; }
|
||||
|
||||
protected string Version { get; set; } = "";
|
||||
|
||||
protected string? SearchText { get; set; }
|
||||
|
||||
protected IEnumerable<Torrent> Torrents => GetTorrents();
|
||||
|
||||
protected bool IsAuthenticated { get; set; }
|
||||
|
||||
protected bool LostConnection { get; set; }
|
||||
|
||||
private List<Torrent> GetTorrents()
|
||||
{
|
||||
if (MainData is null)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
|
||||
|
||||
return MainData.Torrents.Values.Filter(filterState).ToList();
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
if (!await ApiClient.CheckAuthState())
|
||||
{
|
||||
NavigationManager.NavigateTo("/login");
|
||||
return;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
|
||||
Preferences = await ApiClient.GetApplicationPreferences();
|
||||
Version = await ApiClient.GetApplicationVersion();
|
||||
var data = await ApiClient.GetMainData(_requestId);
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
|
||||
_requestId = data.ResponseId;
|
||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||
|
||||
IsAuthenticated = true;
|
||||
|
||||
Menu?.ShowMenu(Preferences);
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_refreshEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (firstRender)
|
||||
{
|
||||
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_refreshInterval)))
|
||||
{
|
||||
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
||||
{
|
||||
if (!IsAuthenticated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
QBitTorrentClient.Models.MainData data;
|
||||
try
|
||||
{
|
||||
data = await ApiClient.GetMainData(_requestId);
|
||||
}
|
||||
catch (HttpRequestException)
|
||||
{
|
||||
if (MainData is not null)
|
||||
{
|
||||
MainData.LostConnection = true;
|
||||
}
|
||||
_timerCancellationToken.CancelIfNotDisposed();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
return;
|
||||
}
|
||||
|
||||
if (MainData is null || data.FullUpdate)
|
||||
{
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
}
|
||||
else
|
||||
{
|
||||
DataManager.MergeMainData(data, MainData);
|
||||
}
|
||||
|
||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||
_requestId = data.ResponseId;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
|
||||
|
||||
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
|
||||
|
||||
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
|
||||
|
||||
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
|
||||
|
||||
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
|
||||
|
||||
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
|
||||
|
||||
protected EventCallback<SortDirection> SortDirectionChanged => EventCallback.Factory.Create<SortDirection>(this, sortDirection => SortDirection = sortDirection);
|
||||
|
||||
protected static (string, Color) GetConnectionIcon(string? status)
|
||||
{
|
||||
if (status is null)
|
||||
{
|
||||
return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
|
||||
}
|
||||
|
||||
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
|
||||
}
|
||||
|
||||
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,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,11 +0,0 @@
|
||||
@inherits LayoutComponentBase
|
||||
@layout LoggedInLayout
|
||||
|
||||
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
|
||||
<MudNavMenu>
|
||||
<ApplicationActions IsMenu="false" Preferences="Preferences" />
|
||||
</MudNavMenu>
|
||||
</MudDrawer>
|
||||
<MudMainContent>
|
||||
@Body
|
||||
</MudMainContent>
|
||||
@@ -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,41 +0,0 @@
|
||||
namespace Lantean.QBTMud.Models
|
||||
{
|
||||
public class RssList
|
||||
{
|
||||
public RssList(Dictionary<string, RssFeed> feeds, List<RssArticle> articles)
|
||||
{
|
||||
Feeds = feeds;
|
||||
|
||||
foreach (var article in articles)
|
||||
{
|
||||
var feed = Feeds[article.Feed];
|
||||
feed.ArticleCount++;
|
||||
if (!article.IsRead)
|
||||
{
|
||||
feed.UnreadCount++;
|
||||
}
|
||||
|
||||
Articles.Add(article);
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, RssFeed> Feeds { get; }
|
||||
|
||||
public List<RssArticle> Articles { get; } = [];
|
||||
|
||||
public int UnreadCount => Feeds.Values.Sum(f => f.UnreadCount);
|
||||
|
||||
internal void MarkAllUnreadAsRead()
|
||||
{
|
||||
foreach (var feed in Feeds)
|
||||
{
|
||||
feed.Value.UnreadCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
internal void MarkAsUnread(string selectedFeed)
|
||||
{
|
||||
Feeds[selectedFeed].UnreadCount = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,36 +0,0 @@
|
||||
@page "/blocks"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
|
||||
</MudToolBar>
|
||||
|
||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||
<MudCardContent>
|
||||
<EditForm Model="Model" OnSubmit="Submit">
|
||||
<MudGrid>
|
||||
<MudItem md="10">
|
||||
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem md="2">
|
||||
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</EditForm>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<DynamicTable @ref="Table"
|
||||
T="Lantean.QBitTorrentClient.Models.PeerLog"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Results"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
RowClassFunc="RowClass"
|
||||
Class="search-list" />
|
||||
@@ -1,39 +0,0 @@
|
||||
@page "/categories"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudText Class="px-5 no-wrap">Categories</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
|
||||
</MudToolBar>
|
||||
|
||||
<DynamicTable @ref="Table"
|
||||
T="Category"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Results"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
Class="details-list" />
|
||||
|
||||
@code {
|
||||
private RenderFragment<RowContext<Category>> ActionsColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
var value = (Category?)context.GetValue();
|
||||
<MudButtonGroup>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Warning" OnClick="@(e => EditCategory(value?.Name))" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(e => DeleteCategory(value?.Name))" />
|
||||
</MudButtonGroup>
|
||||
;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
@page "/details/{hash}"
|
||||
@layout DetailsLayout
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
@if (Hash is not null)
|
||||
{
|
||||
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
|
||||
}
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">@Name</MudText>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
@if (ShowTabs)
|
||||
{
|
||||
<CascadingValue Value="RefreshInterval" Name="RefreshInterval">
|
||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
|
||||
<MudTabPanel Text="General">
|
||||
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Trackers">
|
||||
<TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Peers">
|
||||
<PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="HTTP Sources">
|
||||
<WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Content">
|
||||
<FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
</CascadingValue>
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
@page "/log"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">Execution Log</MudText>
|
||||
</MudToolBar>
|
||||
|
||||
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
|
||||
<MudCardContent>
|
||||
<EditForm Model="Model" OnSubmit="Submit">
|
||||
<MudGrid>
|
||||
<MudItem md="7">
|
||||
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem md="3">
|
||||
<MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
|
||||
<MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
|
||||
<MudSelectItem Value="@("Info")">Info</MudSelectItem>
|
||||
<MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
|
||||
<MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem md="2">
|
||||
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</EditForm>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
|
||||
<DynamicTable @ref="Table"
|
||||
T="Lantean.QBitTorrentClient.Models.Log"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Results"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
RowClassFunc="RowClass"
|
||||
Class="search-list" />
|
||||
@@ -1,43 +0,0 @@
|
||||
@page "/settings"
|
||||
@layout OtherLayout
|
||||
|
||||
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudText Class="px-5 no-wrap">Settings</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
|
||||
</MudToolBar>
|
||||
|
||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
|
||||
<MudTabPanel Text="Behaviour">
|
||||
<BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Downloads">
|
||||
<DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Connection">
|
||||
<ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Speed">
|
||||
<SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="BitTorrent">
|
||||
<BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="RSS">
|
||||
<RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Web UI">
|
||||
<WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Advanced">
|
||||
<AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
|
||||
</MudTabPanel>
|
||||
</MudTabs>
|
||||
@@ -1,73 +0,0 @@
|
||||
@page "/rss"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudText Class="px-5 no-wrap">RSS</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" Disabled="@(SelectedFeed is null)" title="Mark items read" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
|
||||
</MudToolBar>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
|
||||
<MudGrid Class="rss-contents">
|
||||
<MudItem xs="4" Style="height: 100%">
|
||||
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
|
||||
<MudListItem Icon="@Icons.Material.Filled.MarkEmailUnread" Text="@($"Unread ({UnreadCount})")" Value="@("unread")" />
|
||||
@foreach (var (key, feed) in Feeds)
|
||||
{
|
||||
<MudListItem Icon="@(feed.IsLoading ? Icons.Material.Filled.Sync : Icons.Material.Filled.Wifi)" Class="@(feed.IsLoading ? "spin-animation" : "")" Text="@($"{feed.Title} ({feed.UnreadCount})")" Value="@key" />
|
||||
}
|
||||
</MudList>
|
||||
</MudItem>
|
||||
<MudItem xs="4" Style="height: 100%; overflow: auto">
|
||||
@if (Articles.Count > 0)
|
||||
{
|
||||
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged" Dense>
|
||||
@foreach (var article in Articles)
|
||||
{
|
||||
<MudListItem Text="@article.Title" Value="article.Id" Icon="@Icons.Material.Filled.Check" IconColor="@(article.IsRead ? Color.Success : Color.Transparent)" />
|
||||
}
|
||||
</MudList>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
|
||||
}
|
||||
</MudItem>
|
||||
<MudItem xs="4" Style="height: 100%">
|
||||
@if (Article is not null)
|
||||
{
|
||||
<MudCard>
|
||||
<MudCardHeader>
|
||||
<CardHeaderContent>
|
||||
<MudText Typo="Typo.h6" Style="overflow-wrap: anywhere">@Article.Title</MudText>
|
||||
</CardHeaderContent>
|
||||
<CardHeaderActions>
|
||||
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(Article.TorrentURL)" title="Download">Download</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@Article.TorrentURL" Target="@Article.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
|
||||
</MudMenu>
|
||||
</CardHeaderActions>
|
||||
</MudCardHeader>
|
||||
|
||||
<MudCardContent>
|
||||
<MudText Typo="Typo.subtitle2">@Article.Date</MudText>
|
||||
<MudText Typo="Typo.body1">@Article.Description</MudText>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
@@ -1,239 +0,0 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Pages
|
||||
{
|
||||
public partial class Rss : IAsyncDisposable
|
||||
{
|
||||
private readonly bool _refreshEnabled = true;
|
||||
|
||||
private readonly CancellationTokenSource _timerCancellationToken = new();
|
||||
private bool _disposedValue;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MainData? MainData { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "DrawerOpen")]
|
||||
public bool DrawerOpen { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? Hash { get; set; }
|
||||
|
||||
protected int ActiveTab { get; set; } = 0;
|
||||
|
||||
protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500;
|
||||
|
||||
protected ServerState? ServerState => MainData?.ServerState;
|
||||
|
||||
protected string? SelectedFeed { get; set; }
|
||||
|
||||
protected string? SelectedArticle { get; set; }
|
||||
|
||||
protected RssList? RssList { get; set; }
|
||||
|
||||
protected Dictionary<string, RssFeed> Feeds => RssList?.Feeds ?? [];
|
||||
|
||||
protected List<RssArticle> Articles { get; } = [];
|
||||
|
||||
protected int UnreadCount => RssList?.UnreadCount ?? 0;
|
||||
|
||||
protected RssArticle? Article { get; set; }
|
||||
|
||||
protected void SelectedFeedChanged(string value)
|
||||
{
|
||||
SelectedFeed = value;
|
||||
SelectedArticle = null;
|
||||
|
||||
Articles.Clear();
|
||||
|
||||
if (RssList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
IEnumerable<RssArticle> articles;
|
||||
|
||||
if (value == "unread")
|
||||
{
|
||||
articles = RssList.Articles.Where(a => !a.IsRead);
|
||||
}
|
||||
else
|
||||
{
|
||||
articles = RssList.Articles.Where(a => a.Feed == value);
|
||||
}
|
||||
|
||||
foreach (var article in articles)
|
||||
{
|
||||
Articles.Add(article);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task SelectedArticleChanged(string value)
|
||||
{
|
||||
Article = null;
|
||||
SelectedArticle = value;
|
||||
|
||||
if (RssList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var article = RssList.Articles.Find(a => a.Id == value);
|
||||
if (article is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
article.IsRead = true;
|
||||
Articles.First(a => a.Id == value).IsRead = true;
|
||||
Article = article;
|
||||
|
||||
await ApiClient.MarkRssItemAsRead(article.Feed, article.Id);
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
await GetRssList();
|
||||
}
|
||||
|
||||
private async Task GetRssList()
|
||||
{
|
||||
var items = await ApiClient.GetAllRssItems(true);
|
||||
RssList = DataManager.CreateRssList(items);
|
||||
}
|
||||
|
||||
protected async Task DownloadItem(string? url)
|
||||
{
|
||||
await DialogService.InvokeAddTorrentLinkDialog(ApiClient, url);
|
||||
}
|
||||
|
||||
protected void NavigateBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
protected async Task NewSubscription()
|
||||
{
|
||||
var url = await DialogService.ShowStringFieldDialog("RSS Feed URL", "Feed URL", null);
|
||||
if (url is not null)
|
||||
{
|
||||
await ApiClient.AddRssFeed(url);
|
||||
|
||||
await GetRssList();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task MarkAsRead()
|
||||
{
|
||||
if (SelectedFeed is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SelectedFeed == "unread")
|
||||
{
|
||||
if (RssList is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var articles = RssList.Articles.Where(a => !a.IsRead);
|
||||
foreach (var article in articles)
|
||||
{
|
||||
await ApiClient.MarkRssItemAsRead(article.Feed, article.Id);
|
||||
}
|
||||
|
||||
RssList.MarkAllUnreadAsRead();
|
||||
}
|
||||
else
|
||||
{
|
||||
await ApiClient.MarkRssItemAsRead(SelectedFeed);
|
||||
|
||||
RssList?.MarkAsUnread(SelectedFeed);
|
||||
}
|
||||
|
||||
foreach (var article in Articles)
|
||||
{
|
||||
article.IsRead = true;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task UpdateAll()
|
||||
{
|
||||
foreach (var (path, feed) in Feeds)
|
||||
{
|
||||
feed.IsLoading = true;
|
||||
await ApiClient.RefreshRssItem(path);
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task EditDownloadRules()
|
||||
{
|
||||
await DialogService.InvokeRssRulesDialog();
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_refreshEnabled)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!firstRender)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
|
||||
{
|
||||
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
||||
{
|
||||
await GetRssList();
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected virtual Task DisposeAsync(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
{
|
||||
if (disposing)
|
||||
{
|
||||
_timerCancellationToken.Cancel();
|
||||
_timerCancellationToken.Dispose();
|
||||
}
|
||||
|
||||
_disposedValue = true;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
|
||||
await DisposeAsync(disposing: true);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
@page "/search"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">Search</MudText>
|
||||
</MudToolBar>
|
||||
|
||||
<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" />
|
||||
@@ -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,62 +0,0 @@
|
||||
@page "/statistics"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">Statistics</MudText>
|
||||
</MudToolBar>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents">
|
||||
<MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="All-time uploaded">@DisplayHelpers.Size(ServerState?.AllTimeUploaded)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="All-time downloaded">@DisplayHelpers.Size(ServerState?.AllTimeDownloaded)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="All-time share ratio">@DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00")</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Session waste">@DisplayHelpers.Size(ServerState?.TotalWastedSession)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Connected peers">@DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections)</MudField>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudText Typo="Typo.subtitle2" Class="pt-6">Cache statistics</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Read cache hits">@DisplayHelpers.Percentage(ServerState?.ReadCacheHits)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Total buffer size">@DisplayHelpers.Size(ServerState?.TotalBuffersSize)</MudField>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudText Typo="Typo.subtitle2" Class="pt-6">Performance statistics</MudText>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Write cache overload">@DisplayHelpers.Percentage(ServerState?.WriteCacheOverload)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Read cache overload">@DisplayHelpers.Percentage(ServerState?.ReadCacheOverload)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Queued I/O jobs">@DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs)</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Average time in queue">@DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms")</MudField>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</MudContainer>
|
||||
@@ -1,38 +0,0 @@
|
||||
@page "/tags"
|
||||
@layout OtherLayout
|
||||
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
|
||||
<MudDivider Vertical="true" />
|
||||
}
|
||||
<MudText Class="px-5 no-wrap">Tags</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
|
||||
</MudToolBar>
|
||||
|
||||
<DynamicTable @ref="Table"
|
||||
T="string"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Results"
|
||||
MultiSelection="false"
|
||||
SelectOnRowClick="false"
|
||||
Class="details-list" />
|
||||
|
||||
@code {
|
||||
private RenderFragment<RowContext<string>> ActionsColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
var value = (string?)context.GetValue();
|
||||
<MudButtonGroup>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@(e => DeleteTag(value))" />
|
||||
</MudButtonGroup>
|
||||
;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
@page "/"
|
||||
@layout ListLayout
|
||||
|
||||
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
|
||||
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
|
||||
<MudDivider />
|
||||
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
|
||||
</ContextMenu>
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
|
||||
<MudDivider Vertical="true" />
|
||||
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrentToolbar" title="View torrent details" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
|
||||
<MudSpacer />
|
||||
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
|
||||
<DynamicTable
|
||||
@ref="Table"
|
||||
T="Torrent"
|
||||
Class="torrent-list"
|
||||
ColumnDefinitions="Columns"
|
||||
Items="Torrents"
|
||||
OnRowClick="RowClick"
|
||||
MultiSelection="true"
|
||||
SelectOnRowClick="true"
|
||||
SelectedItemsChanged="SelectedItemsChanged"
|
||||
SortColumnChanged="SortColumnChangedHandler"
|
||||
SortDirectionChanged="SortDirectionChangedHandler"
|
||||
OnTableDataContextMenu="TableDataContextMenu"
|
||||
OnTableDataLongPress="TableDataLongPress"
|
||||
/>
|
||||
</MudContainer>
|
||||
|
||||
@code {
|
||||
private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
var value = (float?)context.GetValue();
|
||||
var color = value < 1 ? Color.Success : Color.Info;
|
||||
<MudProgressLinear title="Progress" Color="@color" Value="@((value ?? 0) * 100)" Class="progress-expand" Size="Size.Large">
|
||||
@DisplayHelpers.Percentage(value)
|
||||
</MudProgressLinear>;
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static RenderFragment<RowContext<Torrent>> IconColumn
|
||||
{
|
||||
get
|
||||
{
|
||||
return context => __builder =>
|
||||
{
|
||||
var (icon, color) = DisplayHelpers.GetStateIcon((string?)context.GetValue());
|
||||
<MudIcon Icon="@icon" Color="@color" />
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
|
||||
namespace Lantean.QBTMud.Services
|
||||
{
|
||||
public interface IDataManager
|
||||
{
|
||||
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
|
||||
|
||||
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
||||
|
||||
void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList);
|
||||
|
||||
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
||||
|
||||
void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList);
|
||||
|
||||
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
||||
|
||||
void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
||||
|
||||
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
||||
|
||||
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
///*!
|
||||
// * long-press-event - v2.4.6
|
||||
// * Pure JavaScript long-press-event
|
||||
// * https://github.com/john-doherty/long-press-event
|
||||
// * @author John Doherty <www.johndoherty.info>
|
||||
// * @license MIT
|
||||
// */
|
||||
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e), this.dispatchEvent(new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY })) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
|
||||
@@ -1,10 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,81 +0,0 @@
|
||||
namespace Lantean.QBitTorrentClient.Models
|
||||
{
|
||||
public class SaveLocation
|
||||
{
|
||||
public bool IsWatchedFolder { get; set; }
|
||||
|
||||
public bool IsDefaltFolder { 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
|
||||
{
|
||||
IsDefaltFolder = true
|
||||
};
|
||||
}
|
||||
}
|
||||
else if (value is string stringValue)
|
||||
{
|
||||
if (stringValue == "0")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsWatchedFolder = true
|
||||
};
|
||||
}
|
||||
else if (stringValue == "1")
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
IsDefaltFolder = true
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SaveLocation
|
||||
{
|
||||
SavePath = stringValue
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(value));
|
||||
}
|
||||
|
||||
public object ToValue()
|
||||
{
|
||||
if (IsWatchedFolder)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
else if (IsDefaltFolder)
|
||||
{
|
||||
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.~~
|
||||
5
global.json
Normal file
5
global.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.306"
|
||||
}
|
||||
}
|
||||
@@ -68,11 +68,13 @@ cd qbtmud
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
### 3. Build the Application
|
||||
### 3. Build and Publish the Application
|
||||
```sh
|
||||
dotnet build --configuration Release
|
||||
dotnet publish --configuration Release
|
||||
```
|
||||
|
||||
This will output the Web UI files to `Lantean.QBTMud\bin\Release\net9.0\publish\wwwroot`.
|
||||
|
||||
### 4. Configure qBittorrent to Use qbtmud
|
||||
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
|
||||
|
||||
|
||||
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.
|
||||
@@ -1,4 +1,4 @@
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Router AppAssembly="@typeof(App).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
||||
55
src/Lantean.QBTMud/Components/ApplicationActions.razor
Normal file
55
src/Lantean.QBTMud/Components/ApplicationActions.razor
Normal file
@@ -0,0 +1,55 @@
|
||||
@if (IsMenu)
|
||||
{
|
||||
@foreach (var action in Actions)
|
||||
{
|
||||
if (action.SeparatorBefore)
|
||||
{
|
||||
<MudDivider />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(action.Href))
|
||||
{
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="@action.Color" Href="@action.Href">@action.Text</MudMenuItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="@action.Color" OnClick="action.Callback">@action.Text</MudMenuItem>
|
||||
}
|
||||
}
|
||||
<MudDivider />
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow"
|
||||
IconColor="Color.Success"
|
||||
Disabled="@(_startAllInProgress || MainData?.LostConnection == true)"
|
||||
OnClick="StartAllTorrents">
|
||||
Start all torrents
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Stop"
|
||||
IconColor="Color.Warning"
|
||||
Disabled="@(_stopAllInProgress || MainData?.LostConnection == true)"
|
||||
OnClick="StopAllTorrents">
|
||||
Stop all torrents
|
||||
</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Undo" OnClick="ResetWebUI">Reset Web UI</MudMenuItem>
|
||||
<MudDivider />
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.Logout" OnClick="Logout">Logout</MudMenuItem>
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" OnClick="Exit">Exit qBittorrent</MudMenuItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavLink Icon="@Icons.Material.Outlined.Navigation" OnClick="NavigateBack">Torrents</MudNavLink>
|
||||
<MudDivider />
|
||||
@foreach (var action in Actions)
|
||||
{
|
||||
if (action.SeparatorBefore)
|
||||
{
|
||||
<MudDivider />
|
||||
}
|
||||
@if (!string.IsNullOrEmpty(action.Href))
|
||||
{
|
||||
<MudNavLink Icon="@action.Icon" IconColor="@action.Color" Href="@action.Href">@action.Text</MudNavLink>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudNavLink Icon="@action.Icon" IconColor="@action.Color" OnClick="action.Callback">@action.Text</MudNavLink>
|
||||
}
|
||||
}
|
||||
}
|
||||
230
src/Lantean.QBTMud/Components/ApplicationActions.razor.cs
Normal file
230
src/Lantean.QBTMud/Components/ApplicationActions.razor.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Interop;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.JSInterop;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Components
|
||||
{
|
||||
public partial class ApplicationActions
|
||||
{
|
||||
private List<UIAction>? _actions;
|
||||
private bool _startAllInProgress;
|
||||
private bool _stopAllInProgress;
|
||||
private bool _registerMagnetHandlerInProgress;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected ISnackbar Snackbar { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IJSRuntime JSRuntime { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public bool IsMenu { get; set; }
|
||||
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
public Preferences? Preferences { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public Lantean.QBTMud.Models.MainData? MainData { get; set; }
|
||||
|
||||
protected IEnumerable<UIAction> Actions => GetActions();
|
||||
|
||||
private IEnumerable<UIAction> GetActions()
|
||||
{
|
||||
if (_actions is not null)
|
||||
{
|
||||
foreach (var action in _actions)
|
||||
{
|
||||
if (action.Name != "rss" || Preferences is not null && Preferences.RssProcessingEnabled)
|
||||
{
|
||||
yield return action;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_actions =
|
||||
[
|
||||
new("statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"),
|
||||
new("search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"),
|
||||
new("rss", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"),
|
||||
new("log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"),
|
||||
new("blocks", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"),
|
||||
new("cookies", "Cookie Manager", Icons.Material.Filled.Cookie, Color.Default, "/cookies"),
|
||||
new("registerMagnetHandler", "Register magnet handler", CustomIcons.Magnet, Color.Default, EventCallback.Factory.Create(this, RegisterMagnetHandler)),
|
||||
new("tags", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true),
|
||||
new("categories", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"),
|
||||
new("settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true),
|
||||
new("about", "About", Icons.Material.Filled.Info, Color.Default, "/about"),
|
||||
];
|
||||
}
|
||||
|
||||
protected void NavigateBack()
|
||||
{
|
||||
NavigationManager.NavigateTo("/");
|
||||
}
|
||||
|
||||
protected async Task ResetWebUI()
|
||||
{
|
||||
var preferences = new UpdatePreferences
|
||||
{
|
||||
AlternativeWebuiEnabled = false,
|
||||
};
|
||||
|
||||
await ApiClient.SetApplicationPreferences(preferences);
|
||||
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
}
|
||||
|
||||
protected async Task Logout()
|
||||
{
|
||||
await DialogWorkflow.ShowConfirmDialog("Logout?", "Are you sure you want to logout?", async () =>
|
||||
{
|
||||
await ApiClient.Logout();
|
||||
|
||||
NavigationManager.NavigateTo("/", true);
|
||||
});
|
||||
}
|
||||
|
||||
protected async Task Exit()
|
||||
{
|
||||
await DialogWorkflow.ShowConfirmDialog("Quit?", "Are you sure you want to exit qBittorrent?", ApiClient.Shutdown);
|
||||
}
|
||||
|
||||
private async Task RegisterMagnetHandler()
|
||||
{
|
||||
if (_registerMagnetHandlerInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_registerMagnetHandlerInProgress = true;
|
||||
|
||||
try
|
||||
{
|
||||
var templateUrl = BuildMagnetHandlerTemplateUrl();
|
||||
var result = await JSRuntime.RegisterMagnetHandler(templateUrl);
|
||||
|
||||
var status = (result.Status ?? string.Empty).ToLowerInvariant();
|
||||
switch (status)
|
||||
{
|
||||
case "success":
|
||||
Snackbar?.Add("Magnet handler registered. Magnet links will now open in qBittorrent WebUI.", Severity.Success);
|
||||
break;
|
||||
|
||||
case "insecure":
|
||||
Snackbar?.Add("Access this WebUI over HTTPS to register the magnet handler.", Severity.Warning);
|
||||
break;
|
||||
|
||||
case "unsupported":
|
||||
Snackbar?.Add("This browser does not support registering magnet handlers.", Severity.Warning);
|
||||
break;
|
||||
|
||||
default:
|
||||
var message = string.IsNullOrWhiteSpace(result.Message)
|
||||
? "Unable to register the magnet handler."
|
||||
: $"Unable to register the magnet handler: {result.Message}";
|
||||
Snackbar?.Add(message, Severity.Error);
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (JSException exception)
|
||||
{
|
||||
Snackbar?.Add($"Unable to register the magnet handler: {exception.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_registerMagnetHandlerInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task StartAllTorrents()
|
||||
{
|
||||
if (_startAllInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MainData?.LostConnection == true)
|
||||
{
|
||||
Snackbar?.Add("qBittorrent client is not reachable.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_startAllInProgress = true;
|
||||
try
|
||||
{
|
||||
await ApiClient.StartAllTorrents();
|
||||
Snackbar?.Add("All torrents started.", Severity.Success);
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
Snackbar?.Add($"Unable to start torrents: {exception.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_startAllInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task StopAllTorrents()
|
||||
{
|
||||
if (_stopAllInProgress)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (MainData?.LostConnection == true)
|
||||
{
|
||||
Snackbar?.Add("qBittorrent client is not reachable.", Severity.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
_stopAllInProgress = true;
|
||||
try
|
||||
{
|
||||
await ApiClient.StopAllTorrents();
|
||||
Snackbar?.Add("All torrents stopped.", Severity.Info);
|
||||
}
|
||||
catch (HttpRequestException exception)
|
||||
{
|
||||
Snackbar?.Add($"Unable to stop torrents: {exception.Message}", Severity.Error);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_stopAllInProgress = false;
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildMagnetHandlerTemplateUrl()
|
||||
{
|
||||
var baseUri = NavigationManager.BaseUri;
|
||||
if (string.IsNullOrEmpty(baseUri))
|
||||
{
|
||||
return "#download=%s";
|
||||
}
|
||||
|
||||
var trimmedBase = baseUri.EndsWith("/", StringComparison.Ordinal)
|
||||
? baseUri[..^1]
|
||||
: baseUri;
|
||||
|
||||
return $"{trimmedBase}/#download=%s";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using Lantean.QBitTorrentClient.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected HashSet<string> Tags { get; } = [];
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent" MaximumFileCount="50" >
|
||||
<ActivatorContent>
|
||||
<MudButton Variant="Variant.Filled"
|
||||
Color="Color.Primary"
|
||||
StartIcon="@Icons.Material.Filled.CloudUpload">
|
||||
Choose files
|
||||
</MudButton>
|
||||
</ActivatorContent>
|
||||
</MudFileUpload>
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
@if (Files?.Any() == true)
|
||||
{
|
||||
<MudCard Elevation="3" Class="mb-3">
|
||||
<MudCardHeader Class="pb-0">
|
||||
Selected Torrent Files (@Files.Count)
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
<MudList T="string">
|
||||
@foreach (var file in Files)
|
||||
{
|
||||
<MudListItem>
|
||||
<div class="w-100 d-flex align-center">
|
||||
<span>@file.Name</span>
|
||||
<MudSpacer />
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Delete"
|
||||
Color="Color.Error"
|
||||
Variant="Variant.Text"
|
||||
Size="Size.Small"
|
||||
OnClick="@(() => Remove(file))" />
|
||||
</div>
|
||||
</MudListItem>
|
||||
}
|
||||
</MudList>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
}
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption="true" />
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Close</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Submit">Upload Torrents</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Forms;
|
||||
using MudBlazor;
|
||||
@@ -8,15 +8,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class AddTorrentFileDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
|
||||
private List<IBrowserFile> Files { get; set; } = [];
|
||||
|
||||
protected AddTorrentOptions TorrentOptions { get; set; } = default!;
|
||||
|
||||
protected void UploadFiles(IReadOnlyList<IBrowserFile> files)
|
||||
{
|
||||
Files = files;
|
||||
Files = files.ToList();
|
||||
}
|
||||
|
||||
protected void Cancel()
|
||||
@@ -30,6 +30,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
MudDialog.Close(DialogResult.Ok(options));
|
||||
}
|
||||
|
||||
protected void Remove(IBrowserFile file)
|
||||
{
|
||||
Files.Remove(file);
|
||||
}
|
||||
|
||||
protected override Task Submit(KeyboardEvent keyboardEvent)
|
||||
{
|
||||
Submit();
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Url { get; set; }
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class AddTrackerDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected HashSet<string> Trackers { get; } = [];
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private string _savePath = string.Empty;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
@@ -1,4 +1,4 @@
|
||||
@typeparam T
|
||||
@typeparam T
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ConfirmDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string Content { get; set; } = default!;
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class DeleteDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public int Count { get; set; }
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
@if (Exception is null)
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Components.Dialogs
|
||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ExceptionDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public Exception? Exception { get; set; }
|
||||
@@ -1,4 +1,4 @@
|
||||
@typeparam T
|
||||
@typeparam T
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog ContentStyle="mix-width: 400px">
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Filter;
|
||||
using Lantean.QBTMud.Filter;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -11,10 +11,10 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<string> Hashes { get; set; } = [];
|
||||
@@ -106,7 +106,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected async Task AddCategory()
|
||||
{
|
||||
var addedCategoy = await DialogService.InvokeAddCategoryDialog(ApiClient);
|
||||
var addedCategoy = await DialogWorkflow.InvokeAddCategoryDialog();
|
||||
if (addedCategoy is null)
|
||||
{
|
||||
return;
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -11,10 +11,10 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<string> Hashes { get; set; } = [];
|
||||
@@ -102,7 +102,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected async Task AddTag()
|
||||
{
|
||||
var addedTags = await DialogService.ShowAddTagsDialog();
|
||||
var addedTags = await DialogWorkflow.ShowAddTagsDialog();
|
||||
|
||||
if (addedTags is null || addedTags.Count == 0)
|
||||
{
|
||||
@@ -1,4 +1,4 @@
|
||||
@inherits SubmittableDialog
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class MultipleFieldDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = default!;
|
||||
@@ -1,4 +1,4 @@
|
||||
@typeparam T
|
||||
@typeparam T
|
||||
@inherits SubmittableDialog
|
||||
|
||||
<MudDialog>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Numerics;
|
||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
||||
{
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Label { get; set; }
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="4">
|
||||
@@ -1,4 +1,4 @@
|
||||
using Blazored.LocalStorage;
|
||||
using Blazored.LocalStorage;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
@@ -24,13 +24,13 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDataManager DataManager { get; set; } = default!;
|
||||
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Hash { get; set; }
|
||||
@@ -426,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
{
|
||||
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -495,7 +494,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
{
|
||||
var oldPath = renamedFile.Path + renamedFile.OriginalName;
|
||||
var newPath = renamedFile.Path + renamedFile.NewName;
|
||||
|
||||
|
||||
await ApiClient.RenameFolder(Hash, oldPath, newPath);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<MudDialog>
|
||||
<MudDialog>
|
||||
<DialogContent>
|
||||
<MudGrid>
|
||||
<MudItem xs="3">
|
||||
@@ -53,7 +53,7 @@
|
||||
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="string" Label="Add paused" Value="AddPaused" ValueChanged="AddPausedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
||||
<MudSelect T="string" Label="Add stopped" Value="AddStopped" ValueChanged="AddStoppedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
|
||||
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
||||
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
||||
@@ -103,4 +103,4 @@
|
||||
<MudButton OnClick="Cancel">Close</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
@@ -10,11 +10,14 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private readonly List<string> _unsavedRuleNames = [];
|
||||
|
||||
[CascadingParameter]
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
||||
@@ -114,11 +117,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
SelectedRule.IgnoreDays = value;
|
||||
}
|
||||
|
||||
protected string? AddPaused { get; set; }
|
||||
protected string? AddStopped { get; set; }
|
||||
|
||||
protected void AddPausedChanged(string value)
|
||||
protected void AddStoppedChanged(string value)
|
||||
{
|
||||
AddPaused = value;
|
||||
AddStopped = value;
|
||||
switch (value)
|
||||
{
|
||||
case "default":
|
||||
@@ -194,7 +197,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected async Task AddRule()
|
||||
{
|
||||
var ruleName = await DialogService.ShowStringFieldDialog("Add Rule", "Name", null);
|
||||
var ruleName = await DialogWorkflow.ShowStringFieldDialog("Add Rule", "Name", null);
|
||||
if (ruleName is null)
|
||||
{
|
||||
return;
|
||||
@@ -273,15 +276,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
switch (SelectedRule.TorrentParams.Stopped)
|
||||
{
|
||||
case null:
|
||||
AddPaused = "default";
|
||||
AddStopped = "default";
|
||||
break;
|
||||
|
||||
case true:
|
||||
AddPaused = "always";
|
||||
AddStopped = "always";
|
||||
break;
|
||||
|
||||
case false:
|
||||
AddPaused = "never";
|
||||
AddStopped = "never";
|
||||
break;
|
||||
}
|
||||
|
||||
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,239 @@
|
||||
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>
|
||||
<DialogContent>
|
||||
@@ -34,10 +35,19 @@
|
||||
<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" />
|
||||
</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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<MudButton OnClick="Cancel">Cancel</MudButton>
|
||||
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
|
||||
</DialogActions>
|
||||
</MudDialog>
|
||||
</MudDialog>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user