mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-11-04 22:13:14 +00:00
Compare commits
124 Commits
v0.1.0
...
feature/v5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdea74c2d2 | ||
|
|
2dec9fcaa4 | ||
|
|
c8a4664620 | ||
|
|
c569d4fc94 | ||
|
|
a48361662a | ||
|
|
8be784ffe6 | ||
|
|
44fab60189 | ||
|
|
3fd9f67866 | ||
|
|
43d0a8e449 | ||
|
|
05d5d8429e | ||
|
|
7bc21cd5c9 | ||
|
|
932a590a57 | ||
|
|
5fae053b6e | ||
|
|
4ee3f8417d | ||
|
|
22e168926d | ||
|
|
f78ea3d3d4 | ||
|
|
f65183ac6a | ||
|
|
711a800f53 | ||
|
|
c8d9485847 | ||
|
|
5e96cb0e05 | ||
|
|
ccccf537c9 | ||
|
|
c87441a633 | ||
|
|
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 | ||
|
|
4f9129fd46 | ||
|
|
9a9d2c2ee2 | ||
|
|
736bc46745 | ||
|
|
23ae19c4c7 | ||
|
|
603470eb30 | ||
|
|
27c2406340 | ||
|
|
4578dcc11f | ||
|
|
3215fa3936 | ||
|
|
78e62f31d0 | ||
|
|
e23842fcb0 | ||
|
|
411c7f87cc | ||
|
|
4098f8f5a9 | ||
|
|
12f81c5978 | ||
|
|
717738d720 | ||
|
|
885c34c8cf | ||
|
|
ef3c68a6aa | ||
|
|
a29e64fc1b | ||
|
|
e55955c75e | ||
|
|
aa80396862 | ||
|
|
30ced3293c | ||
|
|
c54f73a517 | ||
|
|
bad509e40f | ||
|
|
6a0796ef20 | ||
|
|
dc4b515763 | ||
|
|
938702a7b3 | ||
|
|
6ca1c6edd4 | ||
|
|
24eb5cf5e9 |
@@ -1,4 +1,11 @@
|
|||||||
[*.cs]
|
root = true
|
||||||
|
|
||||||
|
[*]
|
||||||
|
charset = utf-8
|
||||||
|
end_of_line = crlf
|
||||||
|
insert_final_newline = true
|
||||||
|
|
||||||
|
[*.cs]
|
||||||
|
|
||||||
# IDE0290: Use primary constructor
|
# IDE0290: Use primary constructor
|
||||||
csharp_style_prefer_primary_constructors = false
|
csharp_style_prefer_primary_constructors = false
|
||||||
@@ -77,3 +84,4 @@ dotnet_style_operator_placement_when_wrapping = beginning_of_line
|
|||||||
tab_width = 4
|
tab_width = 4
|
||||||
indent_size = 4
|
indent_size = 4
|
||||||
end_of_line = crlf
|
end_of_line = crlf
|
||||||
|
charset = utf-8
|
||||||
99
.github/workflows/dotnet.yml
vendored
99
.github/workflows/dotnet.yml
vendored
@@ -4,6 +4,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
workflow_dispatch:
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
@@ -21,69 +22,123 @@ jobs:
|
|||||||
- name: Setup .NET
|
- name: Setup .NET
|
||||||
uses: actions/setup-dotnet@v4
|
uses: actions/setup-dotnet@v4
|
||||||
with:
|
with:
|
||||||
dotnet-version: '8.0.x'
|
dotnet-version: '9.0.x'
|
||||||
|
|
||||||
- name: Install GitVersion
|
- name: Install GitVersion
|
||||||
uses: gittools/actions/gitversion/setup@v3.0.0
|
uses: gittools/actions/gitversion/setup@v3.0.0
|
||||||
with:
|
with:
|
||||||
versionSpec: '6.x'
|
versionSpec: '6.0.0'
|
||||||
|
|
||||||
- name: Determine Version
|
- name: Determine Version
|
||||||
id: gitversion
|
id: gitversion
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(dotnet gitversion /output json /showvariable FullSemVer)
|
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=$VERSION" >> $GITHUB_ENV
|
||||||
|
echo "VERSION_SAFE=$SAFE_VERSION" >> $GITHUB_ENV
|
||||||
shell: bash
|
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
|
- name: Restore dependencies
|
||||||
run: dotnet restore
|
run: dotnet restore
|
||||||
|
|
||||||
- name: Build
|
- name: Build
|
||||||
run: dotnet build --configuration Release --no-restore
|
run: dotnet build --configuration Release --no-restore
|
||||||
|
|
||||||
- name: Run Tests
|
- name: Run Tests with Coverage
|
||||||
run: dotnet test --no-build --configuration Release
|
run: dotnet test --no-build --configuration Release --collect:"XPlat Code Coverage" --results-directory TestResults
|
||||||
|
|
||||||
- name: Publish (only on master)
|
- name: Install ReportGenerator
|
||||||
if: github.ref == 'refs/heads/master'
|
run: dotnet tool install --global dotnet-reportgenerator-globaltool
|
||||||
run: dotnet publish Lantean.QBTMud/Lantean.QBTMud.csproj -c Release -o output
|
|
||||||
|
- name: Generate Coverage Report
|
||||||
|
run: reportgenerator -reports:"TestResults/**/coverage.cobertura.xml" -targetdir:"coverage-report" -reporttypes:"HtmlInline_AzurePipelines;Cobertura"
|
||||||
|
|
||||||
|
- name: Upload Coverage Artifact
|
||||||
|
if: always()
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: coverage-report
|
||||||
|
path: coverage-report
|
||||||
|
|
||||||
|
- 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
|
- name: Prepare Release ZIP
|
||||||
if: github.ref == 'refs/heads/master'
|
if: steps.release_channel.outputs.channel != 'none'
|
||||||
run: |
|
run: |
|
||||||
cd output
|
cd output
|
||||||
mv wwwroot public
|
mv wwwroot public
|
||||||
zip -r "../qbt-mud-v${{ env.VERSION }}.zip" public
|
zip -r "../qbt-mud-v${{ env.VERSION_SAFE }}.zip" public
|
||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
- name: Check if Tag Exists
|
- name: Resolve Release Tag
|
||||||
id: check_tag
|
if: steps.release_channel.outputs.channel != 'none'
|
||||||
|
id: resolve_tag
|
||||||
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
if git rev-parse "v${{ env.VERSION }}" >/dev/null 2>&1; then
|
if git rev-parse "${VERSION}" >/dev/null 2>&1; then
|
||||||
echo "TAG_EXISTS=true" >> $GITHUB_ENV
|
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
|
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
|
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
|
- 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
|
id: create_release
|
||||||
uses: actions/create-release@v1
|
uses: actions/create-release@v1
|
||||||
with:
|
with:
|
||||||
tag_name: v${{ env.VERSION }}
|
tag_name: ${{ steps.resolve_tag.outputs.tag }}
|
||||||
release_name: Release v${{ env.VERSION }}
|
release_name: ${{ steps.release_channel.outputs.label }} ${{ steps.resolve_tag.outputs.tag }}
|
||||||
draft: false
|
draft: ${{ steps.release_channel.outputs.channel != 'alpha' }}
|
||||||
prerelease: false
|
prerelease: ${{ steps.release_channel.outputs.prerelease }}
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Upload Release Asset
|
- 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
|
uses: actions/upload-release-asset@v1
|
||||||
with:
|
with:
|
||||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||||
asset_path: qbt-mud-v${{ env.VERSION }}.zip
|
asset_path: qbt-mud-v${{ env.VERSION_SAFE }}.zip
|
||||||
asset_name: qbt-mud-v${{ env.VERSION }}.zip
|
asset_name: qbt-mud-v${{ env.VERSION_SAFE }}.zip
|
||||||
asset_content_type: application/zip
|
asset_content_type: application/zip
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|||||||
6
.gitignore
vendored
6
.gitignore
vendored
@@ -361,3 +361,9 @@ MigrationBackup/
|
|||||||
|
|
||||||
# Fody - auto-generated XML schema
|
# Fody - auto-generated XML schema
|
||||||
FodyWeavers.xsd
|
FodyWeavers.xsd
|
||||||
|
/output
|
||||||
|
|
||||||
|
/refs/
|
||||||
|
coverage/
|
||||||
|
coverage-report/
|
||||||
|
TestResults/
|
||||||
|
|||||||
61
AGENTS.md
Normal file
61
AGENTS.md
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
# 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).
|
||||||
|
- Agents must verify the pinned SDK is available in the current environment; if `dotnet --info` does not list the required version, install it (e.g., via `dotnet-install.sh`) before running restore/build/test commands.
|
||||||
|
- 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
|
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||||
# Visual Studio Version 17
|
# Visual Studio Version 18
|
||||||
VisualStudioVersion = 17.8.34511.84
|
VisualStudioVersion = 18.0.11121.172
|
||||||
MinimumVisualStudioVersion = 10.0.40219.1
|
MinimumVisualStudioVersion = 10.0.40219.1
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud.Test", "Lantean.QBTMud.Test\Lantean.QBTMud.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud.Test", "test\Lantean.QBTMud.Test\Lantean.QBTMud.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "src\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud", "Lantean.QBTMud\Lantean.QBTMud.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMud", "src\Lantean.QBTMud\Lantean.QBTMud.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
|
||||||
ProjectSection(SolutionItems) = preProject
|
ProjectSection(SolutionItems) = preProject
|
||||||
.editorconfig = .editorconfig
|
.editorconfig = .editorconfig
|
||||||
|
AGENTS.md = AGENTS.md
|
||||||
readme.md = readme.md
|
readme.md = readme.md
|
||||||
EndProjectSection
|
EndProjectSection
|
||||||
EndProject
|
EndProject
|
||||||
|
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Lantean.QBitTorrentClient.Test", "test\Lantean.QBitTorrentClient.Test\Lantean.QBitTorrentClient.Test.csproj", "{796E865C-7AA6-4BD9-B12F-394801199A75}"
|
||||||
|
EndProject
|
||||||
|
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Test", "Test", "{344EAF42-5D2B-4F56-8B28-1F3158A37E0A}"
|
||||||
|
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
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
Debug|Any CPU = Debug|Any CPU
|
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}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
|
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{796E865C-7AA6-4BD9-B12F-394801199A75}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
|
GlobalSection(NestedProjects) = preSolution
|
||||||
|
{715E075C-1D86-4A7F-BC72-E1E24A294F17} = {344EAF42-5D2B-4F56-8B28-1F3158A37E0A}
|
||||||
|
{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
|
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||||
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
|
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
|
||||||
EndGlobalSection
|
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" LabelPosition="LabelPosition.End" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
<MudCollapse Expanded="Expanded">
|
|
||||||
<MudGrid>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
|
||||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
@if (ShowCookieOption)
|
|
||||||
{
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
}
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
|
|
||||||
@foreach (var category in Categories)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="category">@category</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Start torrent" @bind-Value="StartTorrent" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("None")">None</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
|
|
||||||
</MudItem>
|
|
||||||
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
|
|
||||||
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
|
|
||||||
</MudSelect>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<FieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="12">
|
|
||||||
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
|
|
||||||
</MudItem>
|
|
||||||
</MudGrid>
|
|
||||||
</MudCollapse>
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
|
||||||
using Lantean.QBTMud.Models;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.Dialogs
|
|
||||||
{
|
|
||||||
public partial class AddTorrentOptions
|
|
||||||
{
|
|
||||||
[Inject]
|
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public bool ShowCookieOption { get; set; }
|
|
||||||
|
|
||||||
protected bool Expanded { get; set; }
|
|
||||||
|
|
||||||
protected bool TorrentManagementMode { get; set; }
|
|
||||||
|
|
||||||
protected string SavePath { get; set; } = default!;
|
|
||||||
|
|
||||||
protected string? Cookie { get; set; }
|
|
||||||
|
|
||||||
protected string? RenameTorrent { get; set; }
|
|
||||||
|
|
||||||
protected IEnumerable<string> Categories { get; set; } = [];
|
|
||||||
|
|
||||||
protected string? Category { get; set; }
|
|
||||||
|
|
||||||
protected bool StartTorrent { get; set; } = true;
|
|
||||||
|
|
||||||
protected bool AddToTopOfQueue { get; set; } = true;
|
|
||||||
|
|
||||||
protected string StopCondition { get; set; } = "None";
|
|
||||||
|
|
||||||
protected bool SkipHashCheck { get; set; } = false;
|
|
||||||
|
|
||||||
protected string ContentLayout { get; set; } = "Original";
|
|
||||||
|
|
||||||
protected bool DownloadInSequentialOrder { get; set; } = false;
|
|
||||||
|
|
||||||
protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false;
|
|
||||||
|
|
||||||
protected long DownloadLimit { get; set; }
|
|
||||||
|
|
||||||
protected long UploadLimit { get; set; }
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
var categories = await ApiClient.GetAllCategories();
|
|
||||||
Categories = categories.Select(c => c.Key).ToList();
|
|
||||||
|
|
||||||
var preferences = await ApiClient.GetApplicationPreferences();
|
|
||||||
|
|
||||||
TorrentManagementMode = preferences.AutoTmmEnabled;
|
|
||||||
SavePath = preferences.SavePath;
|
|
||||||
StartTorrent = !preferences.StartPausedEnabled;
|
|
||||||
AddToTopOfQueue = preferences.AddToTopOfQueue;
|
|
||||||
StopCondition = preferences.TorrentStopCondition;
|
|
||||||
ContentLayout = preferences.TorrentContentLayout;
|
|
||||||
}
|
|
||||||
|
|
||||||
public TorrentOptions GetTorrentOptions()
|
|
||||||
{
|
|
||||||
return new TorrentOptions(
|
|
||||||
TorrentManagementMode,
|
|
||||||
SavePath,
|
|
||||||
Cookie,
|
|
||||||
RenameTorrent,
|
|
||||||
Category,
|
|
||||||
StartTorrent,
|
|
||||||
AddToTopOfQueue,
|
|
||||||
StopCondition,
|
|
||||||
SkipHashCheck,
|
|
||||||
ContentLayout,
|
|
||||||
DownloadInSequentialOrder,
|
|
||||||
DownloadFirstAndLastPiecesFirst,
|
|
||||||
DownloadLimit,
|
|
||||||
UploadLimit);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,91 +0,0 @@
|
|||||||
<ContextMenu @ref="ContextMenu" Dense="true">
|
|
||||||
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
|
|
||||||
</ContextMenu>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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,28 +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="@FullWidth"
|
|
||||||
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,294 +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
|
|
||||||
{
|
|
||||||
// This is a very hacky approach but works for now.
|
|
||||||
// This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked.
|
|
||||||
// MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't.
|
|
||||||
// Ideally this should be changed to use something like the way the DialogService works.
|
|
||||||
|
|
||||||
// Or - rework this to have a hidden MudMenu and hook into the OpenChanged event to monitor when the MudMenuItem closes it.
|
|
||||||
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 bool FullWidth { 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 async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (!_isResized)
|
|
||||||
{
|
|
||||||
await DeterminePosition();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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,11 +0,0 @@
|
|||||||
<div class="@Classname">
|
|
||||||
<div @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
|
||||||
@if (!string.IsNullOrEmpty(Icon))
|
|
||||||
{
|
|
||||||
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
|
|
||||||
}
|
|
||||||
<div class="mud-nav-link-text">
|
|
||||||
@ChildContent
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<MudField Variant="Variant.Outlined" InnerPadding="false" Label="@Label" HelperText="@HelperText" Disabled="Disabled">
|
|
||||||
<TickSwitch T="bool" Value="@Value" ValueChanged="ValueChangedCallback" Class="pt-1 pb-1" Disabled="Disabled" Validation="Validation" />
|
|
||||||
</MudField>
|
|
||||||
@@ -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,438 +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)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths)
|
|
||||||
{
|
|
||||||
var parameters = new DialogParameters
|
|
||||||
{
|
|
||||||
{ nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
|
|
||||||
{ nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
|
|
||||||
{ nameof(ColumnOptionsDialog<T>.Widths), widths },
|
|
||||||
};
|
|
||||||
|
|
||||||
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?>))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,28 +0,0 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<TargetFramework>net8.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="8.0.10" />
|
|
||||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
|
||||||
<PackageReference Include="MudBlazor" Version="7.15.0" />
|
|
||||||
<PackageReference Include="MudBlazor.ThemeManager" Version="2.1.0" />
|
|
||||||
<!-- added to fix vuln in dependency -->
|
|
||||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
|
||||||
</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);">
|
|
||||||
@if (MainData?.LostConnection == true)
|
|
||||||
{
|
|
||||||
<MudText Class="mx-2 mb-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
|
||||||
}
|
|
||||||
<MudSpacer />
|
|
||||||
<MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
<MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
@{
|
|
||||||
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
|
||||||
}
|
|
||||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
|
||||||
<MudDivider Vertical="true" />
|
|
||||||
<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);
|
|
||||||
|
|
||||||
_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);
|
|
||||||
}
|
|
||||||
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,38 +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" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
|
|
||||||
<Menu @ref="Menu" />
|
|
||||||
</MudAppBar>
|
|
||||||
<MudDrawer 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">
|
|
||||||
@Body
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</CascadingValue>
|
|
||||||
</MudLayout>
|
|
||||||
</EnhancedErrorBoundary>
|
|
||||||
</CascadingValue>
|
|
||||||
@@ -1,143 +0,0 @@
|
|||||||
using Blazored.LocalStorage;
|
|
||||||
using Lantean.QBTMud.Components;
|
|
||||||
using Microsoft.AspNetCore.Components;
|
|
||||||
using MudBlazor;
|
|
||||||
using MudBlazor.Services;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Layout
|
|
||||||
{
|
|
||||||
public partial class MainLayout : IBrowserViewportObserver, IAsyncDisposable
|
|
||||||
{
|
|
||||||
private const string _isDarkModeStorageKey = "MainLayout.IsDarkMode";
|
|
||||||
private const string _drawerOpenStorageKey = "MainLayout.DrawerOpen";
|
|
||||||
|
|
||||||
private bool _disposedValue;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
|
|
||||||
|
|
||||||
[Inject]
|
|
||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
|
||||||
|
|
||||||
protected bool DrawerOpen { get; set; } = true;
|
|
||||||
|
|
||||||
protected bool ErrorDrawerOpen { get; set; } = false;
|
|
||||||
|
|
||||||
public Guid Id => Guid.NewGuid();
|
|
||||||
|
|
||||||
protected EnhancedErrorBoundary? ErrorBoundary { get; set; }
|
|
||||||
|
|
||||||
protected bool IsDarkMode { get; set; }
|
|
||||||
|
|
||||||
protected MudThemeProvider MudThemeProvider { get; set; } = default!;
|
|
||||||
|
|
||||||
private Menu Menu { get; set; } = default!;
|
|
||||||
|
|
||||||
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
|
|
||||||
{
|
|
||||||
ReportRate = 50,
|
|
||||||
NotifyOnBreakpointOnly = true
|
|
||||||
};
|
|
||||||
|
|
||||||
protected MudTheme Theme { get; set; }
|
|
||||||
|
|
||||||
public MainLayout()
|
|
||||||
{
|
|
||||||
Theme = new MudTheme();
|
|
||||||
Theme.Typography.Default.FontFamily = ["Nunito Sans"];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task ToggleDrawer()
|
|
||||||
{
|
|
||||||
DrawerOpen = !DrawerOpen;
|
|
||||||
await LocalStorage.SetItemAsync(_drawerOpenStorageKey, DrawerOpen);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnParametersSetAsync()
|
|
||||||
{
|
|
||||||
var drawerOpen = await LocalStorage.GetItemAsync<bool?>(_drawerOpenStorageKey);
|
|
||||||
if (drawerOpen is not null)
|
|
||||||
{
|
|
||||||
DrawerOpen = drawerOpen.Value;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
||||||
{
|
|
||||||
if (firstRender)
|
|
||||||
{
|
|
||||||
var isDarkMode = await LocalStorage.GetItemAsync<bool?>(_isDarkModeStorageKey);
|
|
||||||
if (isDarkMode is null)
|
|
||||||
{
|
|
||||||
IsDarkMode = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
IsDarkMode = isDarkMode.Value;
|
|
||||||
}
|
|
||||||
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
|
||||||
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
|
||||||
await InvokeAsync(StateHasChanged);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected Task OnSystemPreferenceChanged(bool value)
|
|
||||||
{
|
|
||||||
IsDarkMode = value;
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
|
||||||
{
|
|
||||||
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
|
|
||||||
{
|
|
||||||
DrawerOpen = false;
|
|
||||||
}
|
|
||||||
else if (browserViewportEventArgs.Breakpoint > Breakpoint.Sm && !DrawerOpen)
|
|
||||||
{
|
|
||||||
DrawerOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void ToggleErrorDrawer()
|
|
||||||
{
|
|
||||||
ErrorDrawerOpen = !ErrorDrawerOpen;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected void Cleared()
|
|
||||||
{
|
|
||||||
ErrorDrawerOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected virtual async Task DisposeAsync(bool disposing)
|
|
||||||
{
|
|
||||||
if (!_disposedValue)
|
|
||||||
{
|
|
||||||
if (disposing)
|
|
||||||
{
|
|
||||||
await BrowserViewportService.UnsubscribeAsync(this);
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposedValue = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected async Task DarkModeChanged(bool value)
|
|
||||||
{
|
|
||||||
IsDarkMode = value;
|
|
||||||
await LocalStorage.SetItemAsync(_isDarkModeStorageKey, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
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,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,39 +0,0 @@
|
|||||||
@page "/details/{hash}"
|
|
||||||
@layout DetailsLayout
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
@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,58 +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="2" 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="2" md="3">
|
|
||||||
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
|
|
||||||
<MudSelectItem Value="@("all")">All</MudSelectItem>
|
|
||||||
<MudDivider />
|
|
||||||
@foreach (var (value, name) in Plugins)
|
|
||||||
{
|
|
||||||
<MudSelectItem Value="value">@name</MudSelectItem>
|
|
||||||
}
|
|
||||||
</MudSelect>
|
|
||||||
</MudItem>
|
|
||||||
<MudItem xs="2" 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,67 +0,0 @@
|
|||||||
@page "/"
|
|
||||||
@layout ListLayout
|
|
||||||
|
|
||||||
<ContextMenu @ref="ContextMenu" Dense="true" AdjustmentX="@(DrawerOpen ? -235 : 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>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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);
|
|
||||||
|
|
||||||
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>net8.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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
nuget.config
Normal file
11
nuget.config
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<configuration>
|
||||||
|
<packageSources>
|
||||||
|
<!-- Define package sources here -->
|
||||||
|
</packageSources>
|
||||||
|
<packageSourceMapping>
|
||||||
|
<!-- Optional source mapping -->
|
||||||
|
</packageSourceMapping>
|
||||||
|
<packageVersionOverride>
|
||||||
|
<package id="FluentAssertions" allowedVersions="[7.0.0,8.0.0)" />
|
||||||
|
</packageVersionOverride>
|
||||||
|
</configuration>
|
||||||
94
readme.md
94
readme.md
@@ -1,14 +1,84 @@
|
|||||||
# qbt-mud
|
# qbtmud
|
||||||
|
|
||||||
## To-Do
|
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
|
||||||
|
|
||||||
- Rename multiple files dialog
|
## Features
|
||||||
- ~~RSS feeds and dialogs~~
|
|
||||||
- ~~About~~
|
qbtmud replicates all core features of the qBittorrent WebUI, including:
|
||||||
- ~~Context menu for files list/trackers list/peers list~~
|
|
||||||
- ~~Tag management page~~
|
- **Torrent Management** – Add, remove, and control torrents.
|
||||||
- ~~Category management page~~
|
- **Tracker Control** – View and manage trackers.
|
||||||
- ~~Update all tables to use DynamicTable~~
|
- **Peer Management** – Monitor and manage peers connected to torrents.
|
||||||
- ~~Log~~
|
- **File Prioritization** – Select and prioritize specific files within a torrent.
|
||||||
- ~~Blocks~~
|
- **Speed Limits** – Set global and per-torrent speed limits.
|
||||||
- ~~Search~~
|
- **RSS Integration** – Subscribe to RSS feeds for automated torrent downloads.
|
||||||
|
- **Search Functionality** – Integrated torrent search.
|
||||||
|
- **Sequential Downloading** – Download files in order for media streaming.
|
||||||
|
- **Super Seeding Mode** – Efficiently distribute torrents as an initial seeder.
|
||||||
|
- **IP Filtering** – Improve security by filtering specific IP addresses.
|
||||||
|
- **IPv6 Support** – Full support for IPv6 networks.
|
||||||
|
- **Bandwidth Scheduler** – Schedule bandwidth limits.
|
||||||
|
- **WebUI Access** – Remotely manage torrents through the WebUI.
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|

|
||||||
|
|
||||||
|
|
||||||
|
For a detailed explanation of these features, refer to the [qBittorrent Options Guide](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
To install qbtmud without building from source:
|
||||||
|
|
||||||
|
### 1. Download the Latest Release
|
||||||
|
- Go to the [qbtmud Releases](https://github.com/lantean-code/qbtmud/releases) page.
|
||||||
|
- Download the latest release archive for your operating system.
|
||||||
|
|
||||||
|
### 2. Extract the Archive
|
||||||
|
- Extract the contents of the downloaded archive to a directory of your choice.
|
||||||
|
|
||||||
|
### 3. Configure qBittorrent to Use qbtmud
|
||||||
|
- Open qBittorrent and navigate to `Tools` > `Options` > `Web UI`.
|
||||||
|
- Enable the option **"Use alternative WebUI"**.
|
||||||
|
- Set the **"Root Folder"** to the directory where you extracted qbtmud.
|
||||||
|
- Click **OK** to save the settings.
|
||||||
|
|
||||||
|
### 4. Access qbtmud
|
||||||
|
- Open your web browser and go to `http://localhost:8080` (or the port configured in qBittorrent).
|
||||||
|
|
||||||
|
For more detailed instructions, refer to the [Alternate WebUI Usage Guide](https://github.com/qbittorrent/qBittorrent/wiki/Alternate-WebUI-usage).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Building from Source
|
||||||
|
|
||||||
|
To build qbtmud from source, you need to have the **.NET 9.0 SDK** installed on your system.
|
||||||
|
|
||||||
|
### 1. Clone the Repository
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/lantean-code/qbtmud.git
|
||||||
|
cd qbtmud
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. Restore Dependencies
|
||||||
|
```sh
|
||||||
|
dotnet restore
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. Build and Publish the Application
|
||||||
|
```sh
|
||||||
|
dotnet publish --configuration Release
|
||||||
|
```
|
||||||
|
|
||||||
|
This will output the Web UI files to `Lantean.QBTMud\bin\Release\net9.0\publish\wwwroot`.
|
||||||
|
|
||||||
|
### 4. Configure qBittorrent to Use qbtmud
|
||||||
|
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
|
||||||
|
|
||||||
|
### 5. Run qbtmud
|
||||||
|
Navigate to the directory containing the built files and run the application using the appropriate command for your OS.
|
||||||
|
|
||||||
|
By following these steps, you can set up qbtmud to manage your qBittorrent server with an improved web interface, offering better functionality and usability.
|
||||||
|
|||||||
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">
|
<Found Context="routeData">
|
||||||
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
|
||||||
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
|
<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('/')
|
||||||
|
? baseUri[..^1]
|
||||||
|
: baseUri;
|
||||||
|
|
||||||
|
return $"{trimmedBase}/#download=%s";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient.Models;
|
using Lantean.QBitTorrentClient.Models;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class AddPeerDialog
|
public partial class AddPeerDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
public IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<PeerId> Peers { get; } = [];
|
protected HashSet<PeerId> Peers { get; } = [];
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> Tags { get; } = [];
|
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;
|
||||||
using Microsoft.AspNetCore.Components.Forms;
|
using Microsoft.AspNetCore.Components.Forms;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -8,15 +8,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class AddTorrentFileDialog
|
public partial class AddTorrentFileDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance 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 AddTorrentOptions TorrentOptions { get; set; } = default!;
|
||||||
|
|
||||||
protected void UploadFiles(IReadOnlyList<IBrowserFile> files)
|
protected void UploadFiles(IReadOnlyList<IBrowserFile> files)
|
||||||
{
|
{
|
||||||
Files = files;
|
Files = files.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void Cancel()
|
protected void Cancel()
|
||||||
@@ -30,6 +30,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
MudDialog.Close(DialogResult.Ok(options));
|
MudDialog.Close(DialogResult.Ok(options));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected void Remove(IBrowserFile file)
|
||||||
|
{
|
||||||
|
Files.Remove(file);
|
||||||
|
}
|
||||||
|
|
||||||
protected override Task Submit(KeyboardEvent keyboardEvent)
|
protected override Task Submit(KeyboardEvent keyboardEvent)
|
||||||
{
|
{
|
||||||
Submit();
|
Submit();
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Lantean.QBTMud.Services;
|
using Lantean.QBTMud.Services;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Url { get; set; }
|
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>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class AddTrackerDialog
|
public partial class AddTrackerDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected HashSet<string> Trackers { get; } = [];
|
protected HashSet<string> Trackers { get; } = [];
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private string _savePath = string.Empty;
|
private string _savePath = string.Empty;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
@typeparam T
|
@typeparam T
|
||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudCard Class="w-100" Elevation="0">
|
<MudCard Class="w-100" Elevation="0">
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
@for (var i = 0; i < Columns.Count; i++)
|
@for (var i = 0; i < OrderedColumns.Length; i++)
|
||||||
{
|
{
|
||||||
var column = Columns[i];
|
var item = OrderedColumns[i];
|
||||||
|
var column = Columns.First(c => c.Id == item);
|
||||||
var index = i;
|
var index = i;
|
||||||
<MudItem xs="7">
|
<MudItem xs="7">
|
||||||
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
|
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPlacement="Placement.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="3">
|
<MudItem xs="3">
|
||||||
<MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
|
<MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ColumnOptionsDialog<T>
|
public partial class ColumnOptionsDialog<T>
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
[EditorRequired]
|
[EditorRequired]
|
||||||
@@ -20,10 +20,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
[Parameter]
|
[Parameter]
|
||||||
public Dictionary<string, int?> Widths { get; set; } = [];
|
public Dictionary<string, int?> Widths { get; set; } = [];
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public Dictionary<string, int> Order { get; set; } = [];
|
||||||
|
|
||||||
protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
|
protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
|
||||||
|
|
||||||
protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
|
protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
|
||||||
|
|
||||||
|
protected Dictionary<string, int> OrderInternal { get; set; } = [];
|
||||||
|
|
||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
if (SelectedColumnsInternal.Count == 0)
|
if (SelectedColumnsInternal.Count == 0)
|
||||||
@@ -51,6 +56,25 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
WidthsInternal[width.Key] = width.Value;
|
WidthsInternal[width.Key] = width.Value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (OrderInternal.Count == 0)
|
||||||
|
{
|
||||||
|
if (Order.Count == 0)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < Columns.Count; i++)
|
||||||
|
{
|
||||||
|
var column = Columns[i];
|
||||||
|
OrderInternal.Add(column.Id, i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var order in Order)
|
||||||
|
{
|
||||||
|
OrderInternal[order.Key] = order.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void SetSelected(bool selected, string id)
|
protected void SetSelected(bool selected, string id)
|
||||||
@@ -101,7 +125,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
|
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
|
||||||
|
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index - 1).Key;
|
||||||
|
|
||||||
|
OrderInternal[otherId] = index;
|
||||||
|
OrderInternal[currentId] = index - 1;
|
||||||
|
|
||||||
|
//(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void MoveDown(int index)
|
protected void MoveDown(int index)
|
||||||
@@ -111,7 +143,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
|
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
|
||||||
|
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index + 1).Key;
|
||||||
|
|
||||||
|
OrderInternal[otherId] = index;
|
||||||
|
OrderInternal[currentId] = index + 1;
|
||||||
|
|
||||||
|
//(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
|
||||||
|
|
||||||
|
StateHasChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string GetValue(int? value, string columnId)
|
protected string GetValue(int? value, string columnId)
|
||||||
@@ -134,6 +174,13 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
return value.Value.ToString();
|
return value.Value.ToString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private string[] OrderedColumns => GetOrderedColumns();
|
||||||
|
|
||||||
|
private string[] GetOrderedColumns()
|
||||||
|
{
|
||||||
|
return OrderInternal.OrderBy(x => x.Value).Select(x => x.Key).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
protected void Cancel()
|
protected void Cancel()
|
||||||
{
|
{
|
||||||
MudDialog.Cancel();
|
MudDialog.Cancel();
|
||||||
@@ -141,7 +188,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected void Submit()
|
protected void Submit()
|
||||||
{
|
{
|
||||||
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal)));
|
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal, OrderInternal)));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override Task Submit(KeyboardEvent keyboardEvent)
|
protected override Task Submit(KeyboardEvent keyboardEvent)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ConfirmDialog
|
public partial class ConfirmDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Content { get; set; } = default!;
|
public string Content { get; set; } = default!;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPosition="LabelPosition.End" />
|
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPlacement="Placement.End" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
</MudGrid>
|
</MudGrid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class DeleteDialog
|
public partial class DeleteDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
@if (Exception is null)
|
@if (Exception is null)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.Dialogs
|
namespace Lantean.QBTMud.Components.Dialogs
|
||||||
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class ExceptionDialog
|
public partial class ExceptionDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public Exception? Exception { get; set; }
|
public Exception? Exception { get; set; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@typeparam T
|
@typeparam T
|
||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog ContentStyle="mix-width: 400px">
|
<MudDialog ContentStyle="mix-width: 400px">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Filter;
|
using Lantean.QBTMud.Filter;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
||||||
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -11,10 +11,10 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IEnumerable<string> Hashes { get; set; } = [];
|
public IEnumerable<string> Hashes { get; set; } = [];
|
||||||
@@ -106,7 +106,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected async Task AddCategory()
|
protected async Task AddCategory()
|
||||||
{
|
{
|
||||||
var addedCategoy = await DialogService.InvokeAddCategoryDialog(ApiClient);
|
var addedCategoy = await DialogWorkflow.InvokeAddCategoryDialog();
|
||||||
if (addedCategoy is null)
|
if (addedCategoy is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -11,10 +11,10 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IEnumerable<string> Hashes { get; set; } = [];
|
public IEnumerable<string> Hashes { get; set; } = [];
|
||||||
@@ -102,7 +102,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected async Task AddTag()
|
protected async Task AddTag()
|
||||||
{
|
{
|
||||||
var addedTags = await DialogService.ShowAddTagsDialog();
|
var addedTags = await DialogWorkflow.ShowAddTagsDialog();
|
||||||
|
|
||||||
if (addedTags is null || addedTags.Count == 0)
|
if (addedTags is null || addedTags.Count == 0)
|
||||||
{
|
{
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class MultipleFieldDialog
|
public partial class MultipleFieldDialog
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string Label { get; set; } = default!;
|
public string Label { get; set; } = default!;
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
@typeparam T
|
@typeparam T
|
||||||
@inherits SubmittableDialog
|
@inherits SubmittableDialog
|
||||||
|
|
||||||
<MudDialog>
|
<MudDialog>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Label { get; set; }
|
public string? Label { get; set; }
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="4">
|
<MudItem xs="4">
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Blazored.LocalStorage;
|
using Blazored.LocalStorage;
|
||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Lantean.QBTMud.Models;
|
using Lantean.QBTMud.Models;
|
||||||
@@ -6,7 +6,6 @@ using Lantean.QBTMud.Services;
|
|||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using static MudBlazor.Colors;
|
|
||||||
|
|
||||||
namespace Lantean.QBTMud.Components.Dialogs
|
namespace Lantean.QBTMud.Components.Dialogs
|
||||||
{
|
{
|
||||||
@@ -25,13 +24,13 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDataManager DataManager { get; set; } = default!;
|
protected ITorrentDataManager DataManager { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public string? Hash { get; set; }
|
public string? Hash { get; set; }
|
||||||
@@ -427,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
{
|
{
|
||||||
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
|
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<MudDialog>
|
<MudDialog>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<MudGrid>
|
<MudGrid>
|
||||||
<MudItem xs="3">
|
<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" />
|
<MudNumericField T="int" Label="Ignore Subsequent Matches for (0 to Disable)" Value="IgnoreDays" ValueChanged="IgnoreDaysChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined" />
|
||||||
</MudItem>
|
</MudItem>
|
||||||
<MudItem xs="12">
|
<MudItem xs="12">
|
||||||
<MudSelect T="string" Label="Add paused" Value="AddPaused" ValueChanged="AddPausedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
<MudSelect T="string" Label="Add stopped" Value="AddStopped" ValueChanged="AddStoppedChanged" Disabled="@(SelectedRuleName is null)" Variant="Variant.Outlined">
|
||||||
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
|
<MudSelectItem Value="@("default")">Use global settings</MudSelectItem>
|
||||||
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
<MudSelectItem Value="@("always")">Always</MudSelectItem>
|
||||||
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
<MudSelectItem Value="@("never")">Never</MudSelectItem>
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
using Lantean.QBitTorrentClient;
|
using Lantean.QBitTorrentClient;
|
||||||
using Lantean.QBTMud.Helpers;
|
using Lantean.QBTMud.Helpers;
|
||||||
using Microsoft.AspNetCore.Components;
|
using Microsoft.AspNetCore.Components;
|
||||||
using MudBlazor;
|
using MudBlazor;
|
||||||
@@ -10,11 +10,14 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
private readonly List<string> _unsavedRuleNames = [];
|
private readonly List<string> _unsavedRuleNames = [];
|
||||||
|
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IDialogService DialogService { get; set; } = default!;
|
protected IDialogService DialogService { get; set; } = default!;
|
||||||
|
|
||||||
|
[Inject]
|
||||||
|
protected IDialogWorkflow DialogWorkflow { get; set; } = default!;
|
||||||
|
|
||||||
[Inject]
|
[Inject]
|
||||||
protected IApiClient ApiClient { get; set; } = default!;
|
protected IApiClient ApiClient { get; set; } = default!;
|
||||||
|
|
||||||
@@ -114,11 +117,11 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
SelectedRule.IgnoreDays = value;
|
SelectedRule.IgnoreDays = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected string? AddPaused { get; set; }
|
protected string? AddStopped { get; set; }
|
||||||
|
|
||||||
protected void AddPausedChanged(string value)
|
protected void AddStoppedChanged(string value)
|
||||||
{
|
{
|
||||||
AddPaused = value;
|
AddStopped = value;
|
||||||
switch (value)
|
switch (value)
|
||||||
{
|
{
|
||||||
case "default":
|
case "default":
|
||||||
@@ -194,7 +197,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
|
|
||||||
protected async Task AddRule()
|
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)
|
if (ruleName is null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
@@ -273,15 +276,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
|||||||
switch (SelectedRule.TorrentParams.Stopped)
|
switch (SelectedRule.TorrentParams.Stopped)
|
||||||
{
|
{
|
||||||
case null:
|
case null:
|
||||||
AddPaused = "default";
|
AddStopped = "default";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case true:
|
case true:
|
||||||
AddPaused = "always";
|
AddStopped = "always";
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case false:
|
case false:
|
||||||
AddPaused = "never";
|
AddStopped = "never";
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
126
src/Lantean.QBTMud/Components/Dialogs/SearchPluginsDialog.razor
Normal file
126
src/Lantean.QBTMud/Components/Dialogs/SearchPluginsDialog.razor
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
<MudDialog Class="search-plugins-dialog">
|
||||||
|
<DialogContent>
|
||||||
|
@if (IsBusy)
|
||||||
|
{
|
||||||
|
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mb-4" />
|
||||||
|
}
|
||||||
|
<MudGrid Class="mb-4" Spacing="2">
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Install from URL</MudText>
|
||||||
|
<MudTextField @bind-Value="InstallUrl"
|
||||||
|
Placeholder="https://example.com/plugin.zip"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
Immediate="true" />
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Disabled="OperationInProgress || string.IsNullOrWhiteSpace(InstallUrl)"
|
||||||
|
OnClick="InstallFromUrl"
|
||||||
|
Class="mt-3"
|
||||||
|
StartIcon="@Icons.Material.Filled.CloudDownload">Install</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
<MudItem xs="12" md="6">
|
||||||
|
<MudPaper Class="pa-4">
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="mb-2">Install from server path</MudText>
|
||||||
|
<MudTextField @bind-Value="InstallLocalPath"
|
||||||
|
Placeholder="/home/qbt/plugins/my-plugin.py"
|
||||||
|
Variant="Variant.Outlined"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
Immediate="true" />
|
||||||
|
<MudButton Color="Color.Primary"
|
||||||
|
Variant="Variant.Filled"
|
||||||
|
Disabled="OperationInProgress || string.IsNullOrWhiteSpace(InstallLocalPath)"
|
||||||
|
OnClick="InstallFromPath"
|
||||||
|
Class="mt-3"
|
||||||
|
StartIcon="@Icons.Material.Filled.UploadFile">Install</MudButton>
|
||||||
|
</MudPaper>
|
||||||
|
</MudItem>
|
||||||
|
</MudGrid>
|
||||||
|
<MudPaper Class="pa-3 mb-3">
|
||||||
|
<MudStack Row="true" Spacing="2">
|
||||||
|
<MudButton Variant="Variant.Filled"
|
||||||
|
Color="Color.Primary"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.ToggleOn"
|
||||||
|
OnClick="EnableSelected">Enable</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Default"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.ToggleOff"
|
||||||
|
OnClick="DisableSelected">Disable</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Error"
|
||||||
|
Disabled="!HasSelection || OperationInProgress"
|
||||||
|
StartIcon="@Icons.Material.Filled.Delete"
|
||||||
|
OnClick="UninstallSelected">Uninstall</MudButton>
|
||||||
|
<MudButton Variant="Variant.Outlined"
|
||||||
|
Color="Color.Info"
|
||||||
|
Disabled="OperationInProgress || Plugins.Count == 0"
|
||||||
|
StartIcon="@Icons.Material.Filled.Update"
|
||||||
|
OnClick="UpdateAll">Update all</MudButton>
|
||||||
|
<MudSpacer />
|
||||||
|
<MudTooltip Text="Refresh plugins">
|
||||||
|
<MudIconButton Icon="@Icons.Material.Filled.Refresh"
|
||||||
|
Color="Color.Default"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="RefreshPlugins" />
|
||||||
|
</MudTooltip>
|
||||||
|
</MudStack>
|
||||||
|
</MudPaper>
|
||||||
|
<MudTable T="Lantean.QBitTorrentClient.Models.SearchPlugin"
|
||||||
|
Items="Plugins"
|
||||||
|
Hover="true"
|
||||||
|
Bordered="true"
|
||||||
|
Dense="true">
|
||||||
|
<HeaderContent>
|
||||||
|
<MudTh>Select</MudTh>
|
||||||
|
<MudTh>Enabled</MudTh>
|
||||||
|
<MudTh>Name</MudTh>
|
||||||
|
<MudTh>Identifier</MudTh>
|
||||||
|
<MudTh>Version</MudTh>
|
||||||
|
<MudTh>Last update</MudTh>
|
||||||
|
<MudTh>Source</MudTh>
|
||||||
|
</HeaderContent>
|
||||||
|
<RowTemplate>
|
||||||
|
<MudTd DataLabel="Select">
|
||||||
|
<MudIconButton Icon="@GetSelectionIcon(context)"
|
||||||
|
Color="@GetSelectionColor(context)"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="@(() => ToggleSelection(context))" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Enabled">
|
||||||
|
<MudIconButton Icon="@GetEnabledIcon(context)"
|
||||||
|
Color="@GetEnabledColor(context)"
|
||||||
|
Disabled="OperationInProgress"
|
||||||
|
OnClick="@(() => TogglePlugin(context, !context.Enabled))" />
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Name">
|
||||||
|
<MudText Typo="Typo.body2">@context.FullName</MudText>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Identifier">
|
||||||
|
<MudChip T="string" Color="Color.Secondary" Variant="Variant.Outlined" Size="Size.Small">@context.Name</MudChip>
|
||||||
|
</MudTd>
|
||||||
|
<MudTd DataLabel="Version">@context.Version</MudTd>
|
||||||
|
<MudTd DataLabel="Last update">@GetLastUpdatedText(context)</MudTd>
|
||||||
|
<MudTd DataLabel="Source">
|
||||||
|
@if (string.IsNullOrWhiteSpace(context.Url))
|
||||||
|
{
|
||||||
|
<MudText Typo="Typo.caption">Not provided</MudText>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<MudLink Href="@context.Url" Target="_blank">@context.Url</MudLink>
|
||||||
|
}
|
||||||
|
</MudTd>
|
||||||
|
</RowTemplate>
|
||||||
|
<NoRecordsContent>
|
||||||
|
<MudText Typo="Typo.subtitle2" Class="pa-4">No plugins installed.</MudText>
|
||||||
|
</NoRecordsContent>
|
||||||
|
</MudTable>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<MudButton Variant="Variant.Text" Color="Color.Primary" OnClick="CloseDialog">Close</MudButton>
|
||||||
|
</DialogActions>
|
||||||
|
</MudDialog>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user