36 Commits

Author SHA1 Message Date
ahjephson
b8412bb232 Add new v5 properties to AddTorrent 2025-10-22 14:05:13 +01:00
ahjephson
e64a13c7c9 Add ShareRatio to include ShareLimitAction 2025-10-22 12:42:30 +01:00
ahjephson
e4ea79a8ed Update planning doc 2025-10-22 11:41:06 +01:00
ahjephson
0976b72411 Add new v5 torrent properties 2025-10-22 11:39:04 +01:00
ahjephson
965fbcd010 Add copy comments/content path 2025-10-22 10:55:34 +01:00
ahjephson
3d0dbde9f4 Add v5 only preferences 2025-10-22 10:40:14 +01:00
ahjephson
5b4fbde7b2 Removed paused state. 2025-10-22 08:29:43 +01:00
ahjephson
0db0ad4374 Remove other v4 logic 2025-10-21 14:20:25 +01:00
ahjephson
c390d83e4d Update to use v5 api only. 2025-10-21 13:38:50 +01:00
ahjephson
8dd29c238d Update client to use net v5 apis 2025-10-21 13:12:38 +01:00
ahjephson
fca17edfd1 Merge tag '1.2.0' into develop
1.2.0
2025-10-20 20:56:10 +01:00
ahjephson
d8535fa262 Merge branch 'release/1.2.0' 2025-10-20 20:55:26 +01:00
ahjephson
1c6bfed6ee Merge pull request #11 from lantean-code/feature/performance-enhancements
Feature/performance enhancements
2025-10-20 20:53:42 +01:00
ahjephson
281caf8026 Format fix correctly 2025-10-20 20:43:47 +01:00
ahjephson
ff905e7cac Fix tab indicator 2025-10-20 20:42:52 +01:00
ahjephson
cb80dd0d6b Fix tabs issue 2025-10-20 20:01:11 +01:00
ahjephson
9113fb90ee Fix statusbar on ios 2025-10-20 18:44:18 +01:00
ahjephson
d8b4e932d1 Fix tabs in About 2025-10-20 17:40:40 +01:00
ahjephson
3d0d211d10 Update layout to remove hacks and rely only on flexbox 2025-10-20 16:39:20 +01:00
ahjephson
7db4f2f78d Fix final issues with longpress. Update all files to use correct encoding and ran through CodeMaid. 2025-10-20 14:54:31 +01:00
ahjephson
1f606b4449 Try fix issue with text selection on longpress 2025-10-20 13:48:05 +01:00
ahjephson
88d66b4887 Fix longpress issue 2025-10-20 13:44:57 +01:00
ahjephson
2ad7be1073 Remove custom ContextMenu and replace with MudMenu 2025-10-20 13:30:40 +01:00
ahjephson
300e81345c Fix status update performance 2025-10-20 11:03:43 +01:00
ahjephson
9d8d84168e Merge pull request #7 from lantean-code/codex/find-and-fix-a-bug
Fix typo in SaveLocation property
2025-10-20 10:15:48 +01:00
ahjephson
bb66b97f45 Merge branch 'develop' into codex/find-and-fix-a-bug 2025-10-20 10:14:47 +01:00
ahjephson
4824037ba7 Fix connection icon 2025-10-20 10:04:50 +01:00
ahjephson
1f9b631a36 Merge bugfixes in 2025-10-20 09:52:55 +01:00
ahjephson
2c744cd972 Fix issue wtih toolbar 2025-10-19 19:13:09 +01:00
ahjephson
b02bb7cfae Fix issues with toolbar not updating 2025-10-19 19:12:10 +01:00
ahjephson
e4dac8556e Improve torrent list performance 2025-10-19 15:21:22 +01:00
ahjephson
a9a8a4eba8 Improve file list performance. 2025-10-19 14:19:21 +01:00
ahjephson
d4ac79af00 Merge pull request #10 from lantean-code/feature/bugfixes
- Fixed an issue where the tag wasn't being correctly applied to the filter in qBittorrent 5.1+ (#9)
- Fixed an issue where the category wasn't being applied to the filter correctly (#9)
- Fixed invalid ValueChanged for "Default Torrent Management Mode"
- Fixed a crash where TimeSpan.FromSeconds was crashing
- Fixed an invalid icon to appear when Paused/Stopped
2025-10-18 16:18:10 +01:00
ahjephson
7370d73c59 Fix minor display issues 2025-10-18 16:01:53 +01:00
ahjephson
8796cc0f24 Fix #9 and bug related to invalid TimeSpan in duration 2025-10-18 15:37:04 +01:00
ahjephson
4eaa46b2b3 Fix property name in SaveLocation 2025-06-03 09:02:47 +01:00
112 changed files with 4793 additions and 1919 deletions

3
.gitignore vendored
View File

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

View File

@@ -10,11 +10,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="AwesomeAssertions" Version="9.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>

View File

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

View File

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

View File

@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
protected HashSet<string> Tags { get; } = [];

View File

@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class AddTorrentFileDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];

View File

@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected IKeyboardService KeyboardService { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Url { get; set; }

View File

@@ -1,33 +1,50 @@
<MudGrid>
@using Lantean.QBitTorrentClient.Models
<MudGrid>
<MudItem xs="12">
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
</MudItem>
</MudGrid>
<MudCollapse Expanded="Expanded">
<MudGrid>
<MudGrid Class="mt-2">
<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 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 Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
<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"></MudTextField>
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined" />
</MudItem>
}
<MudItem xs="12">
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
@foreach (var category in Categories)
<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">@category</MudSelectItem>
<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>
@@ -38,7 +55,7 @@
<FieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
<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>
@@ -47,22 +64,58 @@
<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" />
<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">
<MudItem xs="12" sm="6">
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<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>

View File

@@ -1,12 +1,26 @@
using Lantean.QBitTorrentClient;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
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!;
@@ -17,15 +31,25 @@ namespace Lantean.QBTMud.Components.Dialogs
protected bool TorrentManagementMode { get; set; }
protected string SavePath { get; set; } = default!;
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 IEnumerable<string> Categories { get; set; } = [];
protected IReadOnlyList<CategoryOption> CategoryOptions => _categoryOptions;
protected string? Category { get; set; }
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;
@@ -33,41 +57,264 @@ namespace Lantean.QBTMud.Components.Dialogs
protected string StopCondition { get; set; } = "None";
protected bool SkipHashCheck { get; set; } = false;
protected bool SkipHashCheck { get; set; }
protected string ContentLayout { get; set; } = "Original";
protected bool DownloadInSequentialOrder { get; set; } = false;
protected bool DownloadInSequentialOrder { get; set; }
protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false;
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();
Categories = categories.Select(c => c.Key).ToList();
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;
SavePath = preferences.SavePath;
StartTorrent = !preferences.StartPausedEnabled;
_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 Task SetTorrentManagementMode(bool value)
{
if (TorrentManagementMode == value)
{
return Task.CompletedTask;
}
TorrentManagementMode = value;
if (TorrentManagementMode)
{
ApplyAutomaticPaths();
}
else
{
RestoreManualPaths();
}
return Task.CompletedTask;
}
protected Task SavePathChanged(string value)
{
SavePath = value;
if (!TorrentManagementMode)
{
_manualSavePath = value;
}
return Task.CompletedTask;
}
protected Task SetUseDownloadPath(bool value)
{
if (TorrentManagementMode)
{
return Task.CompletedTask;
}
_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;
}
return Task.CompletedTask;
}
protected Task DownloadPathChanged(string value)
{
DownloadPath = value;
if (!TorrentManagementMode && UseDownloadPath)
{
_manualDownloadPath = value;
}
return Task.CompletedTask;
}
protected Task CategoryChanged(string? value)
{
Category = string.IsNullOrWhiteSpace(value) ? null : value;
if (TorrentManagementMode)
{
ApplyAutomaticPaths();
}
return Task.CompletedTask;
}
protected Task SelectedTagsChanged(IEnumerable<string> tags)
{
SelectedTags = tags is null
? new HashSet<string>(StringComparer.Ordinal)
: new HashSet<string>(tags, StringComparer.Ordinal);
return Task.CompletedTask;
}
protected Task StopConditionChanged(string value)
{
StopCondition = value;
return Task.CompletedTask;
}
protected Task ContentLayoutChanged(string value)
{
ContentLayout = value;
return Task.CompletedTask;
}
protected Task ShareLimitModeChanged(ShareLimitMode mode)
{
SelectedShareLimitMode = mode;
if (mode != ShareLimitMode.Custom)
{
RatioLimitEnabled = false;
SeedingTimeLimitEnabled = false;
InactiveSeedingTimeLimitEnabled = false;
SelectedShareLimitAction = ShareLimitAction.Default;
}
return Task.CompletedTask;
}
protected Task RatioLimitEnabledChanged(bool value)
{
RatioLimitEnabled = value;
return Task.CompletedTask;
}
protected Task RatioLimitChanged(float value)
{
RatioLimit = value;
return Task.CompletedTask;
}
protected Task SeedingTimeLimitEnabledChanged(bool value)
{
SeedingTimeLimitEnabled = value;
return Task.CompletedTask;
}
protected Task SeedingTimeLimitChanged(int value)
{
SeedingTimeLimit = value;
return Task.CompletedTask;
}
protected Task InactiveSeedingTimeLimitEnabledChanged(bool value)
{
InactiveSeedingTimeLimitEnabled = value;
return Task.CompletedTask;
}
protected Task InactiveSeedingTimeLimitChanged(int value)
{
InactiveSeedingTimeLimit = value;
return Task.CompletedTask;
}
protected Task ShareLimitActionChanged(ShareLimitAction value)
{
SelectedShareLimitAction = value;
return Task.CompletedTask;
}
public TorrentOptions GetTorrentOptions()
{
return new TorrentOptions(
var options = new TorrentOptions(
TorrentManagementMode,
SavePath,
_manualSavePath,
Cookie,
RenameTorrent,
Category,
string.IsNullOrWhiteSpace(Category) ? null : Category,
StartTorrent,
AddToTopOfQueue,
StopCondition,
@@ -77,6 +324,152 @@ namespace Lantean.QBTMud.Components.Dialogs
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);
}
}
}

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class AddTrackerDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
protected HashSet<string> Trackers { get; } = [];

View File

@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
private string _savePath = string.Empty;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject]
protected IApiClient ApiClient { get; set; } = default!;

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class ConfirmDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Content { get; set; } = default!;

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class DeleteDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public int Count { get; set; }

View File

@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class ExceptionDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public Exception? Exception { get; set; }

View File

@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
protected IReadOnlyList<PropertyInfo> Columns => _properties;

View File

@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];

View File

@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class MultipleFieldDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Label { get; set; } = default!;

View File

@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Label { get; set; }

View File

@@ -30,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected ILocalStorageService LocalStorage { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Hash { get; set; }
@@ -426,7 +426,6 @@ namespace Lantean.QBTMud.Components.Dialogs
{
await LocalStorage.RemoveItemAsync(_preferencesStorageKey);
}
}
protected override async Task OnInitializedAsync()
@@ -495,7 +494,7 @@ namespace Lantean.QBTMud.Components.Dialogs
{
var oldPath = renamedFile.Path + renamedFile.OriginalName;
var newPath = renamedFile.Path + renamedFile.NewName;
await ApiClient.RenameFolder(Hash, oldPath, newPath);
}

View File

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

View File

@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
private readonly List<string> _unsavedRuleNames = [];
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
@@ -114,11 +114,11 @@ namespace Lantean.QBTMud.Components.Dialogs
SelectedRule.IgnoreDays = value;
}
protected string? AddPaused { get; set; }
protected string? AddStopped { get; set; }
protected void AddPausedChanged(string value)
protected void AddStoppedChanged(string value)
{
AddPaused = value;
AddStopped = value;
switch (value)
{
case "default":
@@ -273,15 +273,15 @@ namespace Lantean.QBTMud.Components.Dialogs
switch (SelectedRule.TorrentParams.Stopped)
{
case null:
AddPaused = "default";
AddStopped = "default";
break;
case true:
AddPaused = "always";
AddStopped = "always";
break;
case false:
AddPaused = "never";
AddStopped = "never";
break;
}

View File

@@ -1,4 +1,5 @@
@inherits SubmittableDialog
@inherits SubmittableDialog
@using Lantean.QBitTorrentClient.Models
<MudDialog>
<DialogContent>
@@ -34,10 +35,19 @@
<MudItem xs="9">
<MudNumericField T="int" Value="InactiveMinutes" ValueChanged="InactiveMinutesChanged" Disabled="@(!(CustomEnabled && InactiveMinutesEnabled))" Min="1" Max="1024000" Variant="Variant.Outlined" Adornment="Adornment.End" AdornmentText="minutes" />
</MudItem>
<MudItem xs="12">
<MudSelect T="ShareLimitAction" Label="Action when limit is reached" Value="SelectedShareLimitAction" ValueChanged="ShareLimitActionChanged" Disabled="@(!CustomEnabled)" Variant="Variant.Outlined">
<MudSelectItem Value="ShareLimitAction.Default">Default</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.Stop">Stop torrent</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.Remove">Remove torrent</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.RemoveWithContent">Remove torrent and data</MudSelectItem>
<MudSelectItem Value="ShareLimitAction.EnableSuperSeeding">Enable super seeding</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>
</MudDialog>

View File

@@ -1,4 +1,7 @@
using Lantean.QBitTorrentClient;
using System;
using System.Collections.Generic;
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -8,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class ShareRatioDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Label { get; set; }
@@ -16,6 +19,9 @@ namespace Lantean.QBTMud.Components.Dialogs
[Parameter]
public ShareRatioMax? Value { get; set; }
[Parameter]
public ShareRatioMax? CurrentValue { get; set; }
[Parameter]
public bool Disabled { get; set; }
@@ -33,6 +39,8 @@ namespace Lantean.QBTMud.Components.Dialogs
protected int InactiveMinutes { get; set; }
protected ShareLimitAction SelectedShareLimitAction { get; set; } = ShareLimitAction.Default;
protected bool CustomEnabled => ShareRatioType == 0;
protected void RatioEnabledChanged(bool value)
@@ -65,40 +73,75 @@ namespace Lantean.QBTMud.Components.Dialogs
InactiveMinutes = value;
}
protected void ShareLimitActionChanged(ShareLimitAction value)
{
SelectedShareLimitAction = value;
}
protected override void OnParametersSet()
{
if (Value is null || Value.RatioLimit == Limits.GlobalLimit && Value.SeedingTimeLimit == Limits.GlobalLimit && Value.InactiveSeedingTimeLimit == Limits.GlobalLimit)
RatioEnabled = false;
TotalMinutesEnabled = false;
InactiveMinutesEnabled = false;
var baseline = Value ?? CurrentValue;
SelectedShareLimitAction = baseline?.ShareLimitAction ?? ShareLimitAction.Default;
if (baseline is null || baseline.RatioLimit == Limits.GlobalLimit && baseline.SeedingTimeLimit == Limits.GlobalLimit && baseline.InactiveSeedingTimeLimit == Limits.GlobalLimit)
{
ShareRatioType = Limits.GlobalLimit;
return;
}
else if (Value.MaxRatio == Limits.NoLimit && Value.MaxSeedingTime == Limits.NoLimit && Value.MaxInactiveSeedingTime == Limits.NoLimit)
if (baseline.MaxRatio == Limits.NoLimit && baseline.MaxSeedingTime == Limits.NoLimit && baseline.MaxInactiveSeedingTime == Limits.NoLimit)
{
ShareRatioType = Limits.NoLimit;
return;
}
ShareRatioType = 0;
if (baseline.RatioLimit >= 0)
{
RatioEnabled = true;
Ratio = baseline.RatioLimit;
}
else
{
ShareRatioType = 0;
if (Value.RatioLimit >= 0)
{
RatioEnabled = true;
Ratio = Value.RatioLimit;
}
if (Value.SeedingTimeLimit >= 0)
{
TotalMinutesEnabled = true;
TotalMinutes = (int)Value.SeedingTimeLimit;
}
if (Value.InactiveSeedingTimeLimit >= 0)
{
InactiveMinutesEnabled = true;
InactiveMinutes = (int)Value.InactiveSeedingTimeLimit;
}
Ratio = 0;
}
if (baseline.SeedingTimeLimit >= 0)
{
TotalMinutesEnabled = true;
TotalMinutes = (int)baseline.SeedingTimeLimit;
}
else
{
TotalMinutes = 0;
}
if (baseline.InactiveSeedingTimeLimit >= 0)
{
InactiveMinutesEnabled = true;
InactiveMinutes = (int)baseline.InactiveSeedingTimeLimit;
}
else
{
InactiveMinutes = 0;
}
}
protected void ShareRatioTypeChanged(int value)
{
ShareRatioType = value;
if (!CustomEnabled)
{
RatioEnabled = false;
TotalMinutesEnabled = false;
InactiveMinutesEnabled = false;
SelectedShareLimitAction = ShareLimitAction.Default;
}
}
protected void Cancel()
@@ -112,16 +155,19 @@ namespace Lantean.QBTMud.Components.Dialogs
if (ShareRatioType == Limits.GlobalLimit)
{
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.GlobalLimit;
result.ShareLimitAction = ShareLimitAction.Default;
}
else if (ShareRatioType == Limits.NoLimit)
{
result.RatioLimit = result.SeedingTimeLimit = result.InactiveSeedingTimeLimit = Limits.NoLimit;
result.ShareLimitAction = ShareLimitAction.Default;
}
else
{
result.RatioLimit = RatioEnabled ? Ratio : Limits.NoLimit;
result.SeedingTimeLimit = TotalMinutesEnabled ? TotalMinutes : Limits.NoLimit;
result.InactiveSeedingTimeLimit = InactiveMinutesEnabled ? InactiveMinutes : Limits.NoLimit;
result.ShareLimitAction = SelectedShareLimitAction;
}
MudDialog.Close(DialogResult.Ok(result));
}
@@ -133,4 +179,4 @@ namespace Lantean.QBTMud.Components.Dialogs
return Task.CompletedTask;
}
}
}
}

View File

@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class SliderFieldDialog<T> where T : struct, INumber<T>
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Label { get; set; }

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class StringFieldDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Label { get; set; }

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class SubMenuDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public UIAction? ParentAction { get; set; }

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class TorrentOptionsDialog
{
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
[EditorRequired]

View File

@@ -1,46 +1,49 @@
<ContextMenu @ref="ContextMenu" Dense="true">
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
</ContextMenu>
</MudMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudDivider Vertical="true" />
<MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
<MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
<MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
<MudSpacer />
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar content-panel__toolbar--scroll">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudDivider Vertical="true" />
<MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
<MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
<MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
<MudSpacer />
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
</div>
<div class="content-panel__body">
<DynamicTable
@ref="Table"
T="ContentItem"
ColumnDefinitions="Columns"
Items="Files"
MultiSelection="false"
SelectOnRowClick="true"
PreSorted="true"
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChanged"
SortDirectionChanged="SortDirectionChanged"
OnTableDataContextMenu="TableDataContextMenu"
OnTableDataLongPress="TableDataLongPress"
Class="file-list content-panel__table"
/>
</div>
</div>
<DynamicTable
@ref="Table"
T="ContentItem"
ColumnDefinitions="Columns"
Items="Files"
MultiSelection="false"
SelectOnRowClick="true"
PreSorted="true"
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChanged"
SortDirectionChanged="SortDirectionChanged"
OnTableDataContextMenu="TableDataContextMenu"
OnTableDataLongPress="TableDataLongPress"
Class="file-list"
/>
@code {
private RenderFragment<RowContext<ContentItem>> NameColumn
{

View File

@@ -8,7 +8,6 @@ using Lantean.QBTMud.Models;
using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System;
using System.Collections.ObjectModel;
using System.Net;
@@ -21,6 +20,9 @@ namespace Lantean.QBTMud.Components
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
private static readonly ReadOnlyCollection<ContentItem> EmptyContentItems = new ReadOnlyCollection<ContentItem>(Array.Empty<ContentItem>());
private ReadOnlyCollection<ContentItem> _visibleFiles = EmptyContentItems;
private bool _filesDirty = true;
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
@@ -66,7 +68,7 @@ namespace Lantean.QBTMud.Components
private DynamicTable<ContentItem>? Table { get; set; }
private ContextMenu? ContextMenu { get; set; }
private MudMenu? ContextMenu { get; set; }
public FilesTab()
{
@@ -103,6 +105,7 @@ namespace Lantean.QBTMud.Components
if (_filterDefinitions is null)
{
Filters = null;
MarkFilesDirty();
return;
}
@@ -114,11 +117,13 @@ namespace Lantean.QBTMud.Components
}
Filters = filters;
MarkFilesDirty();
}
protected void RemoveFilter()
{
Filters = null;
MarkFilesDirty();
}
public async ValueTask DisposeAsync()
@@ -158,6 +163,7 @@ namespace Lantean.QBTMud.Components
protected void SearchTextChanged(string value)
{
SearchText = value;
MarkFilesDirty();
}
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
@@ -179,7 +185,9 @@ namespace Lantean.QBTMud.Components
return;
}
await ContextMenu.OpenMenuAsync(eventArgs);
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
@@ -198,6 +206,7 @@ namespace Lantean.QBTMud.Components
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
var hasUpdates = false;
if (Active && Hash is not null)
{
IReadOnlyList<QBitTorrentClient.Models.FileData> files;
@@ -214,14 +223,20 @@ namespace Lantean.QBTMud.Components
if (FileList is null)
{
FileList = DataManager.CreateContentsList(files);
hasUpdates = true;
}
else
{
DataManager.MergeContentsList(files, FileList);
hasUpdates = DataManager.MergeContentsList(files, FileList);
}
}
await InvokeAsync(StateHasChanged);
if (hasUpdates)
{
MarkFilesDirty();
PruneSelectionIfMissing();
await InvokeAsync(StateHasChanged);
}
}
}
}
@@ -247,6 +262,8 @@ namespace Lantean.QBTMud.Components
var contents = await ApiClient.GetTorrentContents(Hash);
FileList = DataManager.CreateContentsList(contents);
MarkFilesDirty();
PruneSelectionIfMissing();
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
if (expandedNodes is not null)
@@ -257,6 +274,8 @@ namespace Lantean.QBTMud.Components
{
ExpandedNodes.Clear();
}
MarkFilesDirty();
}
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
@@ -321,11 +340,13 @@ namespace Lantean.QBTMud.Components
protected void SortColumnChanged(string sortColumn)
{
_sortColumn = sortColumn;
MarkFilesDirty();
}
protected void SortDirectionChanged(SortDirection sortDirection)
{
_sortDirection = sortDirection;
MarkFilesDirty();
}
protected void SelectedItemChanged(ContentItem item)
@@ -344,6 +365,7 @@ namespace Lantean.QBTMud.Components
ExpandedNodes.Add(contentItem.Name);
}
MarkFilesDirty();
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
}
@@ -369,44 +391,6 @@ namespace Lantean.QBTMud.Components
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
}
private IEnumerable<ContentItem> GetChildren(ContentItem folder)
{
var childLevel = folder.Level + 1;
var prefix = string.Concat(folder.Name, Extensions.DirectorySeparator);
foreach (var item in FileList!.Values.Where(f => f.Level == childLevel && f.Name.StartsWith(prefix, StringComparison.Ordinal)).OrderByDirection(_sortDirection, GetSortSelector()))
{
if (item.IsFolder)
{
var descendants = GetChildren(item);
// if the filter returns some results then show folder item
if (descendants.Any())
{
yield return item;
}
// if the folder is not expanded - don't return children
if (!ExpandedNodes.Contains(item.Name))
{
continue;
}
// then show children
foreach (var descendant in descendants)
{
yield return descendant;
}
}
else
{
if (FilterContentItem(item))
{
yield return item;
}
}
}
}
private bool FilterContentItem(ContentItem item)
{
if (Filters is not null)
@@ -430,37 +414,130 @@ namespace Lantean.QBTMud.Components
}
private ReadOnlyCollection<ContentItem> GetFiles()
{
if (!_filesDirty)
{
return _visibleFiles;
}
_visibleFiles = BuildVisibleFiles();
_filesDirty = false;
return _visibleFiles;
}
private ReadOnlyCollection<ContentItem> BuildVisibleFiles()
{
if (FileList is null || FileList.Values.Count == 0)
{
return new ReadOnlyCollection<ContentItem>([]);
return EmptyContentItems;
}
var maxLevel = FileList.Values.Max(f => f.Level);
// this is a flat file structure
if (maxLevel == 0)
var lookup = BuildChildrenLookup();
if (!lookup.TryGetValue(string.Empty, out var roots))
{
return FileList.Values.Where(FilterContentItem).OrderByDirection(_sortDirection, GetSortSelector()).ToList().AsReadOnly();
return EmptyContentItems;
}
var list = new List<ContentItem>();
var sortSelector = GetSortSelector();
var orderedRoots = roots.OrderByDirection(_sortDirection, sortSelector).ToList();
var result = new List<ContentItem>(FileList.Values.Count);
var rootItems = FileList.Values.Where(c => c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
foreach (var item in rootItems)
foreach (var item in orderedRoots)
{
list.Add(item);
if (item.IsFolder && ExpandedNodes.Contains(item.Name))
if (item.IsFolder)
{
var descendants = GetChildren(item);
foreach (var descendant in descendants)
result.Add(item);
if (!ExpandedNodes.Contains(item.Name))
{
list.Add(descendant);
continue;
}
var descendants = GetVisibleDescendants(item, lookup, sortSelector);
result.AddRange(descendants);
}
else
{
if (FilterContentItem(item))
{
result.Add(item);
}
}
}
return list.AsReadOnly();
return new ReadOnlyCollection<ContentItem>(result);
}
private Dictionary<string, List<ContentItem>> BuildChildrenLookup()
{
var lookup = new Dictionary<string, List<ContentItem>>(FileList!.Count);
foreach (var item in FileList!.Values)
{
var parentPath = item.Level == 0 ? string.Empty : item.Name.GetDirectoryPath();
if (!lookup.TryGetValue(parentPath, out var children))
{
children = [];
lookup[parentPath] = children;
}
children.Add(item);
}
return lookup;
}
private List<ContentItem> GetVisibleDescendants(ContentItem folder, Dictionary<string, List<ContentItem>> lookup, Func<ContentItem, object?> sortSelector)
{
if (!lookup.TryGetValue(folder.Name, out var children))
{
return [];
}
var orderedChildren = children.OrderByDirection(_sortDirection, sortSelector).ToList();
var visible = new List<ContentItem>();
foreach (var child in orderedChildren)
{
if (child.IsFolder)
{
var descendants = GetVisibleDescendants(child, lookup, sortSelector);
if (descendants.Count != 0)
{
visible.Add(child);
if (ExpandedNodes.Contains(child.Name))
{
visible.AddRange(descendants);
}
}
}
else if (FilterContentItem(child))
{
visible.Add(child);
}
}
return visible;
}
private void MarkFilesDirty()
{
_filesDirty = true;
}
private void PruneSelectionIfMissing()
{
if (SelectedItem is not null && (FileList is null || !FileList.ContainsKey(SelectedItem.Name)))
{
SelectedItem = null;
}
if (ContextMenuItem is not null && (FileList is null || !FileList.ContainsKey(ContextMenuItem.Name)))
{
ContextMenuItem = null;
}
}
protected async Task DoNotDownloadLessThan100PercentAvailability()
@@ -552,4 +629,4 @@ namespace Lantean.QBTMud.Components
ColumnDefinitionHelper.CreateColumnDefinition<ContentItem>("Availability", c => c.Availability, c => c.Availability.ToString("0.00")),
];
}
}
}

View File

@@ -1,8 +1,8 @@
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
<MudMenu @ref="StatusContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
@TorrentControls(_statusType)
</ContextMenu>
</MudMenu>
<ContextMenu @ref="CategoryContextMenu" Dense="true" AdjustmentY="-60">
<MudMenu @ref="CategoryContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddCategory">Add category</MudMenuItem>
@if (IsCategoryTarget)
{
@@ -12,9 +12,9 @@
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove unused categories</MudMenuItem>
<MudDivider />
@TorrentControls(_categoryType)
</ContextMenu>
</MudMenu>
<ContextMenu @ref="TagContextMenu" Dense="true" AdjustmentY="-60">
<MudMenu @ref="TagContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Outlined.AddCircle" IconColor="Color.Info" OnClick="AddTag">Add tag</MudMenuItem>
@if (IsTagTarget)
{
@@ -23,13 +23,13 @@
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedTags">Remove unused tags</MudMenuItem>
<MudDivider />
@TorrentControls(_tagType)
</ContextMenu>
</MudMenu>
<ContextMenu @ref="TrackerContextMenu" Dense="true" AdjustmentY="-60">
<MudMenu @ref="TrackerContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveUnusedCategories">Remove tracker</MudMenuItem>
<MudDivider />
@TorrentControls(_trackerType)
</ContextMenu>
</MudMenu>
<MudNavMenu Dense="true">
<MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
@@ -65,9 +65,9 @@
{
return __builder =>
{
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => ResumeTorrents(type))">Resume torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Pause" IconColor="Color.Warning" OnClick="@(e => PauseTorrents(type))">Pause torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.PlayArrow" IconColor="Color.Success" OnClick="@(e => StartTorrents(type))">Start torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Stop" IconColor="Color.Warning" OnClick="@(e => StopTorrents(type))">Stop torrents</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="@(e => RemoveTorrents(type))">Remove torrents</MudMenuItem>
};
}
}
}

View File

@@ -1,11 +1,11 @@
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMud.Components.UI;
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Linq;
namespace Lantean.QBTMud.Components
{
@@ -69,13 +69,13 @@ namespace Lantean.QBTMud.Components
protected Dictionary<string, int> Statuses => GetStatuses();
protected ContextMenu? StatusContextMenu { get; set; }
protected MudMenu? StatusContextMenu { get; set; }
protected ContextMenu? CategoryContextMenu { get; set; }
protected MudMenu? CategoryContextMenu { get; set; }
protected ContextMenu? TagContextMenu { get; set; }
protected MudMenu? TagContextMenu { get; set; }
protected ContextMenu? TrackerContextMenu { get; set; }
protected MudMenu? TrackerContextMenu { get; set; }
protected string? ContextMenuStatus { get; set; }
@@ -154,7 +154,9 @@ namespace Lantean.QBTMud.Components
ContextMenuStatus = value;
return StatusContextMenu.OpenMenuAsync(args);
var normalizedArgs = args.NormalizeForContextMenu();
return StatusContextMenu.OpenMenuAsync(normalizedArgs);
}
protected async Task CategoryValueChanged(string value)
@@ -192,7 +194,9 @@ namespace Lantean.QBTMud.Components
IsCategoryTarget = value != FilterHelper.CATEGORY_ALL && value != FilterHelper.CATEGORY_UNCATEGORIZED;
ContextMenuCategory = value;
return CategoryContextMenu.OpenMenuAsync(args);
var normalizedArgs = args.NormalizeForContextMenu();
return CategoryContextMenu.OpenMenuAsync(normalizedArgs);
}
protected async Task TagValueChanged(string value)
@@ -230,7 +234,9 @@ namespace Lantean.QBTMud.Components
IsTagTarget = value != FilterHelper.TAG_ALL && value != FilterHelper.TAG_UNTAGGED;
ContextMenuTag = value;
return TagContextMenu.OpenMenuAsync(args);
var normalizedArgs = args.NormalizeForContextMenu();
return TagContextMenu.OpenMenuAsync(normalizedArgs);
}
protected async Task TrackerValueChanged(string value)
@@ -267,7 +273,9 @@ namespace Lantean.QBTMud.Components
ContextMenuTracker = value;
return TrackerContextMenu.OpenMenuAsync(args);
var normalizedArgs = args.NormalizeForContextMenu();
return TrackerContextMenu.OpenMenuAsync(normalizedArgs);
}
protected async Task AddCategory()
@@ -345,25 +353,25 @@ namespace Lantean.QBTMud.Components
}
}
protected async Task ResumeTorrents(string type)
protected async Task StartTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.ResumeTorrents(torrents);
await ApiClient.StartTorrents(hashes: torrents.ToArray());
}
protected async Task PauseTorrents(string type)
protected async Task StopTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await ApiClient.PauseTorrents(torrents);
await ApiClient.StopTorrents(hashes: torrents.ToArray());
}
protected async Task RemoveTorrents(string type)
{
var torrents = GetAffectedTorrentHashes(type);
await DialogService.InvokeDeleteTorrentDialog(ApiClient, [.. torrents]);
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, [.. torrents]);
}
private Dictionary<string, int> GetTags()
@@ -470,4 +478,4 @@ namespace Lantean.QBTMud.Components
}
}
}
}
}

View File

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

View File

@@ -68,6 +68,21 @@
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.subtitle2">Confirmation</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<FieldSwitch Label="Confirm torrent recheck" Value="ConfirmTorrentRecheck" ValueChanged="ConfirmTorrentRecheckChanged" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>

View File

@@ -16,6 +16,8 @@ namespace Lantean.QBTMud.Components.Options
protected int SaveResumeDataInterval { get; private set; }
protected int TorrentFileSizeLimit { get; private set; }
protected bool RecheckCompletedTorrents { get; private set; }
protected bool ConfirmTorrentRecheck { get; private set; }
protected string? AppInstanceName { get; private set; }
protected int RefreshInterval { get; private set; }
protected bool ResolvePeerCountries { get; private set; }
@@ -97,6 +99,7 @@ namespace Lantean.QBTMud.Components.Options
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
ConfirmTorrentRecheck = Preferences.ConfirmTorrentRecheck;
AppInstanceName = Preferences.AppInstanceName;
RefreshInterval = Preferences.RefreshInterval;
ResolvePeerCountries = Preferences.ResolvePeerCountries;
@@ -209,6 +212,13 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ConfirmTorrentRecheckChanged(bool value)
{
ConfirmTorrentRecheck = value;
UpdatePreferences.ConfirmTorrentRecheck = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AppInstanceNameChanged(string value)
{
AppInstanceName = value;
@@ -608,4 +618,4 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
}
}
}

View File

@@ -17,6 +17,24 @@
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.subtitle2">Transfer List</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<FieldSwitch Label="Confirm when deleting torrents" Value="ConfirmTorrentDeletion" ValueChanged="ConfirmTorrentDeletionChanged" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Show external IP in status bar" Value="StatusBarExternalIp" ValueChanged="StatusBarExternalIpChanged" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
@@ -71,4 +89,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

@@ -4,6 +4,10 @@ namespace Lantean.QBTMud.Components.Options
{
public partial class BehaviourOptions : Options
{
protected bool ConfirmTorrentDeletion { get; set; }
protected bool StatusBarExternalIp { get; set; }
protected bool FileLogEnabled { get; set; }
protected string? FileLogPath { get; set; }
@@ -27,6 +31,8 @@ namespace Lantean.QBTMud.Components.Options
return false;
}
ConfirmTorrentDeletion = Preferences.ConfirmTorrentDeletion;
StatusBarExternalIp = Preferences.StatusBarExternalIp;
FileLogEnabled = Preferences.FileLogEnabled;
FileLogPath = Preferences.FileLogPath;
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
@@ -39,6 +45,20 @@ namespace Lantean.QBTMud.Components.Options
return true;
}
protected async Task ConfirmTorrentDeletionChanged(bool value)
{
ConfirmTorrentDeletion = value;
UpdatePreferences.ConfirmTorrentDeletion = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task StatusBarExternalIpChanged(bool value)
{
StatusBarExternalIp = value;
UpdatePreferences.StatusBarExternalIp = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogEnabledChanged(bool value)
{
FileLogEnabled = value;
@@ -96,4 +116,4 @@ namespace Lantean.QBTMud.Components.Options
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
}
}
}

View File

@@ -19,7 +19,7 @@
<FieldSwitch Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" />
</MudItem>
<MudItem xs="12">
<FieldSwitch Label="Do not start the download automatically" Value="StartPausedEnabled" ValueChanged="StartPausedEnabledChanged" />
<FieldSwitch Label="Do not start the download automatically" Value="AddStoppedEnabled" ValueChanged="AddStoppedEnabledChanged" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" Variant="Variant.Outlined">
@@ -62,7 +62,7 @@
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" Variant="Variant.Outlined">
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoTmmEnabledChanged" Variant="Variant.Outlined">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>
@@ -306,4 +306,4 @@
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
</MudCard>

View File

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

View File

@@ -1,24 +1,30 @@
<ContextMenu @ref="ContextMenu" Dense="true">
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddPeer">Add peer</MudMenuItem>
@if (ContextMenuItem is not null)
{
<MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" IconColor="Color.Info" OnClick="BanPeerContextMenu">Ban peer</MudMenuItem>
}
</ContextMenu>
</MudMenu>
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddPeer">Add peer</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.DisabledByDefault" Color="Color.Error" OnClick="BanPeerToolbar" Disabled="@(SelectedItem is null)">Ban peer</MudIconButton>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
</MudToolBar>
</div>
<DynamicTable T="Peer"
ColumnDefinitions="Columns"
Items="Peers"
MultiSelection="false"
SelectOnRowClick="true"
OnTableDataLongPress="TableDataLongPress"
OnTableDataContextMenu="TableDataContextMenu"
SelectedItemChanged="SelectedItemChanged"
Class="details-list" />
<div class="content-panel__body">
<DynamicTable T="Peer"
ColumnDefinitions="Columns"
Items="Peers"
MultiSelection="false"
SelectOnRowClick="true"
OnTableDataLongPress="TableDataLongPress"
OnTableDataContextMenu="TableDataContextMenu"
SelectedItemChanged="SelectedItemChanged"
Class="details-list content-panel__table" />
</div>
</div>

View File

@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
protected Peer? SelectedItem { get; set; }
protected ContextMenu? ContextMenu { get; set; }
protected MudMenu? ContextMenu { get; set; }
protected DynamicTable<Peer>? Table { get; set; }
@@ -153,7 +153,9 @@ namespace Lantean.QBTMud.Components
return;
}
await ContextMenu.ToggleMenuAsync(eventArgs);
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
}
protected void SelectedItemChanged(Peer peer)

View File

@@ -37,9 +37,6 @@ namespace Lantean.QBTMud.Components
[Inject]
protected IKeyboardService KeyboardService { get; set; } = default!;
[CascadingParameter(Name = "Version")]
public string? Version { get; set; }
[Parameter]
[EditorRequired]
public IEnumerable<string> Hashes { get; set; } = default!;
@@ -71,14 +68,12 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; }
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
protected override void OnInitialized()
{
_actions =
[
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Start)),
new("stop", "Stop", Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Stop)),
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
@@ -109,6 +104,8 @@ namespace Lantean.QBTMud.Components
new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
new("copyComment", "Comment", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Comment))),
new("copyContentPath", "Content path", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.ContentPath))),
]),
new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
];
@@ -146,32 +143,16 @@ namespace Lantean.QBTMud.Components
OverlayVisible = value;
}
protected async Task Pause()
protected async Task Stop()
{
if (MajorVersion < 5)
{
await ApiClient.PauseTorrents(Hashes);
Snackbar.Add("Torrent paused.");
}
else
{
await ApiClient.StopTorrents(Hashes);
Snackbar.Add("Torrent stopped.");
}
await ApiClient.StopTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent stopped.");
}
protected async Task Resume()
protected async Task Start()
{
if (MajorVersion < 5)
{
await ApiClient.ResumeTorrents(Hashes);
Snackbar.Add("Torrent resumed.");
}
else
{
await ApiClient.StartTorrents(Hashes);
Snackbar.Add("Torrent started.");
}
await ApiClient.StartTorrents(hashes: Hashes.ToArray());
Snackbar.Add("Torrent started.");
}
protected async Task ForceStart()
@@ -182,7 +163,7 @@ namespace Lantean.QBTMud.Components
protected async Task Remove()
{
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray());
var deleted = await DialogService.InvokeDeleteTorrentDialog(ApiClient, Preferences?.ConfirmTorrentDeletion == true, Hashes.ToArray());
if (deleted)
{
@@ -278,7 +259,7 @@ namespace Lantean.QBTMud.Components
protected async Task ForceRecheck()
{
await ApiClient.RecheckTorrents(null, Hashes.ToArray());
await DialogService.ForceRecheckAsync(ApiClient, Hashes, Preferences?.ConfirmTorrentRecheck == true);
}
protected async Task ForceReannounce()
@@ -385,8 +366,8 @@ namespace Lantean.QBTMud.Components
var allAreFirstLastPiecePrio = true;
var thereAreFirstLastPiecePrio = false;
var allAreDownloaded = true;
var allArePaused = true;
var thereArePaused = false;
var allAreStopped = true;
var thereAreStopped = false;
var allAreForceStart = true;
var thereAreForceStart = false;
var allAreSuperSeeding = true;
@@ -424,27 +405,13 @@ namespace Lantean.QBTMud.Components
allAreSuperSeeding = false;
}
if (MajorVersion < 5)
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
if (torrent.State != "pausedUP" && torrent.State != "pausedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
allAreStopped = false;
}
else
{
if (torrent.State != "stoppedUP" && torrent.State != "stoppedDL")
{
allArePaused = false;
}
else
{
thereArePaused = true;
}
thereAreStopped = true;
}
if (!torrent.ForceStart)
@@ -532,7 +499,7 @@ namespace Lantean.QBTMud.Components
actionStates["superSeeding"] = ActionState.Hidden;
}
if (allArePaused)
if (allAreStopped)
{
actionStates["pause"] = ActionState.Hidden;
}
@@ -540,13 +507,11 @@ namespace Lantean.QBTMud.Components
{
actionStates["forceStart"] = ActionState.Hidden;
}
else if (!thereArePaused && !thereAreForceStart)
else if (!thereAreStopped && !thereAreForceStart)
{
actionStates["start"] = ActionState.Hidden;
}
if (MajorVersion >= 5)
{
if (actionStates.TryGetValue("start", out ActionState? startActionState))
{
startActionState.TextOverride = "Start";
@@ -564,7 +529,6 @@ namespace Lantean.QBTMud.Components
{
actionStates["pause"] = new ActionState { TextOverride = "Stop" };
}
}
if (!allAreAutoTmm && thereAreAutoTmm)
{
@@ -706,4 +670,4 @@ namespace Lantean.QBTMud.Components
MenuItems,
}
}
}

View File

@@ -1,4 +1,4 @@
<ContextMenu @ref="ContextMenu" Dense="true">
<MudMenu @ref="ContextMenu" Dense="true" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Filled.AddCircle" IconColor="Color.Info" OnClick="AddTracker">Add trackers</MudMenuItem>
@if (ContextMenuItem is not null)
{
@@ -6,27 +6,33 @@
<MudMenuItem Icon="@Icons.Material.Filled.Delete" IconColor="Color.Error" OnClick="RemoveTrackerContextMenu">Remove tracker</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.FolderCopy" IconColor="Color.Info" OnClick="CopyTrackerUrlContextMenu">Copy tracker url</MudMenuItem>
}
</ContextMenu>
</MudMenu>
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.AddCircle" Color="Color.Info" OnClick="AddTracker">Add trackers</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Edit" Color="Color.Info" OnClick="EditTrackerToolbar" Disabled="@(SelectedItem is null)">Edit tracker URL</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="RemoveTrackerToolbar" Disabled="@(SelectedItem is null)">Remove tracker</MudIconButton>
<MudIconButton Icon="@Icons.Material.Filled.FolderCopy" Color="Color.Info" OnClick="CopyTrackerUrlToolbar" Disabled="@(SelectedItem is null)">Copy tracker url</MudIconButton>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
</MudToolBar>
</div>
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.TorrentTracker"
ColumnDefinitions="Columns"
Items="Trackers"
MultiSelection="false"
SelectOnRowClick="false"
PreSorted="true"
SortDirectionChanged="SortDirectionChanged"
SortColumnChanged="SortColumnChanged"
OnTableDataLongPress="TableDataLongPress"
OnTableDataContextMenu="TableDataContextMenu"
SelectedItemChanged="SelectedItemChanged"
Class="file-list" />
<div class="content-panel__body">
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.TorrentTracker"
ColumnDefinitions="Columns"
Items="Trackers"
MultiSelection="false"
SelectOnRowClick="false"
PreSorted="true"
SortDirectionChanged="SortDirectionChanged"
SortColumnChanged="SortColumnChanged"
OnTableDataLongPress="TableDataLongPress"
OnTableDataContextMenu="TableDataContextMenu"
SelectedItemChanged="SelectedItemChanged"
Class="file-list content-panel__table" />
</div>
</div>

View File

@@ -52,7 +52,7 @@ namespace Lantean.QBTMud.Components
protected TorrentTracker? SelectedItem { get; set; }
protected ContextMenu? ContextMenu { get; set; }
protected MudMenu? ContextMenu { get; set; }
protected DynamicTable<TorrentTracker>? Table { get; set; }
@@ -148,7 +148,9 @@ namespace Lantean.QBTMud.Components
return;
}
await ContextMenu.ToggleMenuAsync(eventArgs);
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
}
protected void SelectedItemChanged(TorrentTracker torrentTracker)
@@ -169,7 +171,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.AddTrackersToTorrent(Hash, trackers);
await ApiClient.AddTrackersToTorrent(trackers, hashes: new[] { Hash });
}
protected Task EditTrackerToolbar()
@@ -209,7 +211,7 @@ namespace Lantean.QBTMud.Components
return;
}
await ApiClient.RemoveTrackers(Hash, [tracker.Url]);
await ApiClient.RemoveTrackers([tracker.Url], hashes: new[] { Hash });
}
protected Task CopyTrackerUrlToolbar()
@@ -301,4 +303,4 @@ namespace Lantean.QBTMud.Components
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -1,26 +0,0 @@
@inherits MudComponentBase
<MudMenu @ref="FakeMenu" Style="display: none" OpenChanged="FakeOpenChanged"></MudMenu>
@* The portal has to include the cascading values inside, because it's not able to teletransport the cascade *@
<MudPopover tracker="@Id"
Open="@_open"
Class="unselectable"
MaxHeight="@MaxHeight"
AnchorOrigin="@AnchorOrigin"
TransformOrigin="@TransformOrigin"
RelativeWidth="@RelativeWidth"
OverflowBehavior="OverflowBehavior.FlipAlways"
Style="@_popoverStyle"
@ontouchend:preventDefault>
<CascadingValue Value="@(FakeMenu)">
@if (_showChildren)
{
<MudList T="object" Class="unselectable" Dense="@Dense">
@ChildContent
</MudList>
}
</CascadingValue>
</MudPopover>
<MudOverlay Visible="@(_open)" LockScroll="@LockScroll" AutoClose="true" OnClosed="@CloseMenuAsync" />

View File

@@ -1,290 +0,0 @@
using Lantean.QBTMud.Interop;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using MudBlazor;
using MudBlazor.Utilities;
namespace Lantean.QBTMud.Components.UI
{
public partial class ContextMenu : MudComponentBase
{
private bool _open;
private bool _showChildren;
private string? _popoverStyle;
private string? _id;
private double _x;
private double _y;
private bool _isResized = false;
private const double _diff = 64;
private string Id
{
get
{
_id ??= Guid.NewGuid().ToString();
return _id;
}
}
[Inject]
public IJSRuntime JSRuntime { get; set; } = default!;
[Inject]
public IPopoverService PopoverService { get; set; } = default!;
/// <summary>
/// If true, compact vertical padding will be applied to all menu items.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public bool Dense { get; set; }
/// <summary>
/// Set to true if you want to prevent page from scrolling when the menu is open
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public bool LockScroll { get; set; }
/// <summary>
/// If true, the list menu will be same width as the parent.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public DropdownWidth RelativeWidth { get; set; }
/// <summary>
/// Sets the max height the menu can have when open.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public int? MaxHeight { get; set; }
/// <summary>
/// Set the anchor origin point to determine where the popover will open from.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public Origin AnchorOrigin { get; set; } = Origin.TopLeft;
/// <summary>
/// Sets the transform origin point for the popover.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public Origin TransformOrigin { get; set; } = Origin.TopLeft;
/// <summary>
/// If true, menu will be disabled.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.Behavior)]
public bool Disabled { get; set; }
/// <summary>
/// Gets or sets whether to show a ripple effect when the user clicks the button. Default is true.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.Appearance)]
public bool Ripple { get; set; } = true;
/// <summary>
/// Determines whether the component has a drop-shadow. Default is true
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.Appearance)]
public bool DropShadow { get; set; } = true;
/// <summary>
/// Add menu items here
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupBehavior)]
public RenderFragment? ChildContent { get; set; }
/// <summary>
/// Fired when the menu <see cref="Open"/> property changes.
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupBehavior)]
public EventCallback<bool> OpenChanged { get; set; }
[Parameter]
public int AdjustmentX { get; set; }
[Parameter]
public int AdjustmentY { get; set; }
protected MudMenu? FakeMenu { get; set; }
protected void FakeOpenChanged(bool value)
{
if (!value)
{
_open = false;
}
StateHasChanged();
}
/// <summary>
/// Opens the menu.
/// </summary>
/// <param name="args">
/// The arguments of the calling mouse/pointer event.
/// </param>
public async Task OpenMenuAsync(EventArgs args)
{
if (Disabled)
{
return;
}
// long press on iOS triggers selection, so clear it
await JSRuntime.ClearSelection();
if (args is not LongPressEventArgs)
{
_showChildren = true;
}
_open = true;
_isResized = false;
StateHasChanged();
var (x, y) = GetPositionFromArgs(args);
_x = x;
_y = y;
SetPopoverStyle(x, y);
StateHasChanged();
await OpenChanged.InvokeAsync(_open);
// long press on iOS triggers selection, so clear it
await JSRuntime.ClearSelection();
if (args is LongPressEventArgs)
{
await Task.Delay(1000);
_showChildren = true;
}
}
/// <summary>
/// Closes the menu.
/// </summary>
public Task CloseMenuAsync()
{
_open = false;
_popoverStyle = null;
StateHasChanged();
return OpenChanged.InvokeAsync(_open);
}
private void SetPopoverStyle(double x, double y)
{
_popoverStyle = $"margin-top: {y.ToPx()}; margin-left: {x.ToPx()};";
}
/// <summary>
/// Toggle the visibility of the menu.
/// </summary>
public async Task ToggleMenuAsync(EventArgs args)
{
if (Disabled)
{
return;
}
if (_open)
{
await CloseMenuAsync();
}
else
{
await OpenMenuAsync(args);
}
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!_isResized)
{
//await DeterminePosition();
}
return Task.CompletedTask;
}
//private async Task DeterminePosition()
//{
// var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
// double? contextMenuHeight = null;
// double? contextMenuWidth = null;
// var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
// var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
// if (popoverSize.Height > 0)
// {
// contextMenuHeight = popoverSize.Height;
// contextMenuWidth = popoverSize.Width;
// }
// else
// {
// return;
// }
// // the bottom position of the popover will be rendered off screen
// if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
// {
// // adjust the top of the context menu
// var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
// _y -= overshoot;
// if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
// {
// MaxHeight = (int)(mainContentSize.Height - _y + _diff);
// }
// }
// if (_x + contextMenuWidth.Value > mainContentSize.Width)
// {
// var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
// _x -= overshoot;
// }
// SetPopoverStyle(_x, _y);
// _isResized = true;
// await InvokeAsync(StateHasChanged);
//}
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
{
double x, y;
if (eventArgs is MouseEventArgs mouseEventArgs)
{
x = mouseEventArgs.ClientX;
y = mouseEventArgs.ClientY;
}
else if (eventArgs is LongPressEventArgs longPressEventArgs)
{
x = longPressEventArgs.ClientX;
y = longPressEventArgs.ClientY;
}
else
{
throw new NotSupportedException("Invalid eventArgs type.");
}
return (x + AdjustmentX, y + AdjustmentY);
}
}
}

View File

@@ -1,5 +1,5 @@
<div class="@Classname">
<div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
<div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
@if (!string.IsNullOrEmpty(Icon))
{
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />

View File

@@ -59,6 +59,7 @@ namespace Lantean.QBTMud.Components.UI
new CssBuilder("mud-nav-link")
.AddClass($"mud-nav-link-disabled", Disabled)
.AddClass("active", Active)
.AddClass("unselectable", OnLongPress.HasDelegate || OnContextMenu.HasDelegate)
.Build();
protected string IconClassname =>

View File

@@ -81,6 +81,8 @@ namespace Lantean.QBTMud.Components.UI
protected HashSet<string> SelectedColumns { get; set; } = [];
private static readonly IReadOnlyList<ColumnDefinition<T>> EmptyColumns = Array.Empty<ColumnDefinition<T>>();
private Dictionary<string, int?> _columnWidths = [];
private Dictionary<string, int> _columnOrder = [];
@@ -89,8 +91,16 @@ namespace Lantean.QBTMud.Components.UI
private SortDirection _sortDirection;
private DateTimeOffset? _suppressRowClickUntil;
private readonly Dictionary<string, TdExtended> _tds = [];
private IReadOnlyList<ColumnDefinition<T>> _visibleColumns = EmptyColumns;
private bool _columnsDirty = true;
private IEnumerable<ColumnDefinition<T>>? _lastColumnDefinitions;
protected override async Task OnInitializedAsync()
{
HashSet<string> selectedColumns;
@@ -109,6 +119,13 @@ namespace Lantean.QBTMud.Components.UI
SelectedColumns = selectedColumns;
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
}
else
{
SelectedColumns = selectedColumns;
}
_lastColumnDefinitions = ColumnDefinitions;
MarkColumnsDirty();
string? sortColumn;
SortDirection sortDirection;
@@ -137,11 +154,24 @@ namespace Lantean.QBTMud.Components.UI
await SortDirectionChanged.InvokeAsync(_sortDirection);
}
MarkColumnsDirty();
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
if (storedColumnsWidths is not null)
{
_columnWidths = storedColumnsWidths;
}
MarkColumnsDirty();
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
{
_lastColumnDefinitions = ColumnDefinitions;
MarkColumnsDirty();
}
}
private IEnumerable<T>? GetOrderedItems()
@@ -165,39 +195,74 @@ namespace Lantean.QBTMud.Components.UI
return Items.OrderByDirection(_sortDirection, sortSelector);
}
protected IEnumerable<ColumnDefinition<T>> GetColumns()
protected IReadOnlyList<ColumnDefinition<T>> GetColumns()
{
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
if (_columnOrder.Count == 0)
if (!_columnsDirty)
{
foreach (var column in filteredColumns)
{
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
}
yield return column;
}
yield break;
return _visibleColumns;
}
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key))
_visibleColumns = BuildVisibleColumns();
_columnsDirty = false;
return _visibleColumns;
}
private IReadOnlyList<ColumnDefinition<T>> BuildVisibleColumns()
{
var filteredColumns = ColumnDefinitions
.Where(c => SelectedColumns.Contains(c.Id))
.Where(ColumnFilter)
.ToList();
if (filteredColumns.Count == 0)
{
if (!columnDictionary.TryGetValue(columnId, out var column))
return EmptyColumns;
}
List<ColumnDefinition<T>> orderedColumns;
if (_columnOrder.Count == 0)
{
orderedColumns = filteredColumns;
}
else
{
var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList();
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
orderedColumns = new List<ColumnDefinition<T>>(filteredColumns.Count);
foreach (var (columnId, _) in orderLookup)
{
continue;
if (!columnDictionary.TryGetValue(columnId, out var column))
{
continue;
}
orderedColumns.Add(column);
}
if (orderedColumns.Count != filteredColumns.Count)
{
var existingIds = new HashSet<string>(orderedColumns.Select(c => c.Id));
foreach (var column in filteredColumns)
{
if (existingIds.Add(column.Id))
{
orderedColumns.Add(column);
}
}
}
}
foreach (var column in orderedColumns)
{
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
}
yield return column;
}
return orderedColumns;
}
private async Task SetSort(string columnId, SortDirection sortDirection)
@@ -223,6 +288,17 @@ namespace Lantean.QBTMud.Components.UI
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
{
if (_suppressRowClickUntil is not null)
{
if (DateTimeOffset.UtcNow <= _suppressRowClickUntil.Value)
{
_suppressRowClickUntil = null;
return;
}
_suppressRowClickUntil = null;
}
if (eventArgs.Item is null)
{
return;
@@ -298,6 +374,7 @@ namespace Lantean.QBTMud.Components.UI
protected Task OnLongPressInternal(LongPressEventArgs eventArgs, string columnId, T item)
{
_suppressRowClickUntil = DateTimeOffset.UtcNow.AddMilliseconds(500);
var data = _tds[columnId];
return OnTableDataLongPress.InvokeAsync(new TableDataLongPressEventArgs<T>(eventArgs, data, item));
}
@@ -316,18 +393,21 @@ namespace Lantean.QBTMud.Components.UI
SelectedColumns = result.SelectedColumns;
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
MarkColumnsDirty();
}
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
{
_columnWidths = result.ColumnWidths;
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
MarkColumnsDirty();
}
if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
{
_columnOrder = result.ColumnOrder;
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
MarkColumnsDirty();
}
}
@@ -368,17 +448,34 @@ namespace Lantean.QBTMud.Components.UI
if (column.Width.HasValue)
{
className = $"overflow-cell {className}";
className = string.IsNullOrWhiteSpace(className)
? "overflow-cell"
: $"overflow-cell {className}";
}
if (OnTableDataContextMenu.HasDelegate)
{
className = $"no-default-context-menu {className}";
className = string.IsNullOrWhiteSpace(className)
? "no-default-context-menu"
: $"no-default-context-menu {className}";
}
if (OnTableDataLongPress.HasDelegate)
{
className = string.IsNullOrWhiteSpace(className)
? "unselectable"
: $"unselectable {className}";
}
return className;
}
private void MarkColumnsDirty()
{
_columnsDirty = true;
_visibleColumns = EmptyColumns;
}
private sealed record SortData
{
public SortData(string sortColumn, SortDirection sortDirection)

View File

@@ -1,5 +1,5 @@
@inherits MudTd
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
<td data-label="@DataLabel" style="@Style" class="@Classname" @attributes="@UserAttributes" @onlongpress="OnLongPressInternal" @onlongpress:preventDefault @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
@ChildContent
</td>
</td>

View File

@@ -1,6 +1,10 @@
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
ColumnDefinitions="Columns"
Items="WebSeeds"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list" />
<div class="content-panel">
<div class="content-panel__body">
<DynamicTable T="Lantean.QBitTorrentClient.Models.WebSeed"
ColumnDefinitions="Columns"
Items="WebSeeds"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list content-panel__table" />
</div>
</div>

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient;
using ShareLimitAction = Lantean.QBitTorrentClient.Models.ShareLimitAction;
using Lantean.QBTMud.Components.Dialogs;
using Lantean.QBTMud.Filter;
using Lantean.QBTMud.Models;
@@ -56,7 +57,7 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Torrents = files;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
foreach (var stream in streams)
{
@@ -74,18 +75,19 @@ namespace Lantean.QBTMud.Helpers
{
addTorrentParams.ContentLayout = Enum.Parse<QBitTorrentClient.Models.TorrentContentLayout>(options.ContentLayout);
}
if (string.IsNullOrEmpty(options.Cookie))
{
addTorrentParams.Cookie = options.Cookie;
}
addTorrentParams.DownloadLimit = options.DownloadLimit;
addTorrentParams.DownloadPath = options.DownloadPath;
if (!string.IsNullOrWhiteSpace(options.DownloadPath))
{
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;
if (!options.TorrentManagementMode)
{
addTorrentParams.SavePath = options.SavePath;
}
addTorrentParams.SeedingTimeLimit = options.SeedingTimeLimit;
addTorrentParams.SequentialDownload = options.DownloadInSequentialOrder;
if (!string.IsNullOrEmpty(options.ShareLimitAction))
@@ -100,7 +102,10 @@ namespace Lantean.QBTMud.Helpers
addTorrentParams.Stopped = !options.StartTorrent;
addTorrentParams.Tags = options.Tags;
addTorrentParams.UploadLimit = options.UploadLimit;
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
if (options.UseDownloadPath.HasValue)
{
addTorrentParams.UseDownloadPath = options.UseDownloadPath;
}
return addTorrentParams;
}
@@ -123,10 +128,10 @@ namespace Lantean.QBTMud.Helpers
var addTorrentParams = CreateAddTorrentParams(options);
addTorrentParams.Urls = options.Urls;
await apiClient.AddTorrent(addTorrentParams);
_ = await apiClient.AddTorrent(addTorrentParams);
}
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
public static async Task<bool> InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, bool confirmTorrentDeletion, params string[] hashes)
{
if (hashes.Length == 0)
{
@@ -138,6 +143,12 @@ namespace Lantean.QBTMud.Helpers
{ nameof(DeleteDialog.Count), hashes.Length }
};
if (!confirmTorrentDeletion)
{
await apiClient.DeleteTorrents(hashes: hashes, deleteFiles: false);
return true;
}
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)
@@ -150,6 +161,28 @@ namespace Lantean.QBTMud.Helpers
return true;
}
public static async Task ForceRecheckAsync(this IDialogService dialogService, IApiClient apiClient, IEnumerable<string> hashes, bool confirmTorrentRecheck)
{
var hashArray = hashes?.ToArray() ?? [];
if (hashArray.Length == 0)
{
return;
}
if (confirmTorrentRecheck)
{
var content = $"Are you sure you want to recheck the selected torrent{(hashArray.Length == 1 ? "" : "s")}?";
var confirmed = await dialogService.ShowConfirmDialog("Force recheck", content);
if (!confirmed)
{
return;
}
}
await apiClient.RecheckTorrents(null, hashArray);
}
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();
@@ -217,21 +250,30 @@ namespace Lantean.QBTMud.Helpers
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<Torrent> torrents)
{
var torrentShareRatios = torrents.Select(t => new ShareRatioMax
var torrentList = torrents.ToList();
if (torrentList.Count == 0)
{
return;
}
var shareRatioValues = torrentList.Select(t => new ShareRatioMax
{
InactiveSeedingTimeLimit = t.InactiveSeedingTimeLimit,
MaxInactiveSeedingTime = t.InactiveSeedingTimeLimit,
MaxInactiveSeedingTime = t.MaxInactiveSeedingTime,
MaxRatio = t.MaxRatio,
MaxSeedingTime = t.MaxSeedingTime,
RatioLimit = t.RatioLimit,
SeedingTimeLimit = t.SeedingTimeLimit,
});
ShareLimitAction = t.ShareLimitAction,
}).ToList();
var torrentsHaveSameShareRatio = torrentShareRatios.Distinct().Count() == 1;
var referenceValue = shareRatioValues[0];
var torrentsHaveSameShareRatio = shareRatioValues.Distinct().Count() == 1;
var parameters = new DialogParameters
{
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? torrentShareRatios.FirstOrDefault() : null },
{ nameof(ShareRatioDialog.Value), torrentsHaveSameShareRatio ? referenceValue : null },
{ nameof(ShareRatioDialog.CurrentValue), referenceValue },
};
var result = await dialogService.ShowAsync<ShareRatioDialog>("Share ratio", parameters, FormDialogOptions);
@@ -243,7 +285,7 @@ namespace Lantean.QBTMud.Helpers
var shareRatio = (ShareRatio)dialogResult.Data;
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, null, torrents.Select(t => t.Hash).ToArray());
await apiClient.SetTorrentShareLimit(shareRatio.RatioLimit, shareRatio.SeedingTimeLimit, shareRatio.InactiveSeedingTimeLimit, shareRatio.ShareLimitAction ?? ShareLimitAction.Default, hashes: torrentList.Select(t => t.Hash).ToArray());
}
public static async Task InvokeStringFieldDialog(this IDialogService dialogService, string title, string label, string? value, Func<string, Task> onSuccess)
@@ -436,4 +478,6 @@ namespace Lantean.QBTMud.Helpers
await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
}
}
}
}

View File

@@ -1,5 +1,6 @@
using ByteSizeLib;
using Lantean.QBTMud.Models;
using Lantean.QBitTorrentClient;
using MudBlazor;
using System.Diagnostics.CodeAnalysis;
using System.Text;
@@ -19,28 +20,28 @@ namespace Lantean.QBTMud.Helpers
{
if (seconds is null)
{
return "";
return string.Empty;
}
if (seconds == 8640000)
const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
var value = seconds.Value;
if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
{
return "∞";
}
if (seconds < 60)
if (value <= 0)
{
return "< 1m";
}
TimeSpan time;
try
var time = TimeSpan.FromSeconds(value);
if (time.TotalMinutes < 1)
{
time = TimeSpan.FromSeconds(seconds.Value);
}
catch
{
return "∞";
return "< 1m";
}
var sb = new StringBuilder();
if (prefix is not null)
{
@@ -404,8 +405,6 @@ namespace Lantean.QBTMud.Helpers
Status.Downloading => (Icons.Material.Filled.Downloading, Color.Success),
Status.Seeding => (Icons.Material.Filled.Upload, Color.Info),
Status.Completed => (Icons.Material.Filled.Check, Color.Default),
Status.Resumed => (Icons.Material.Filled.PlayArrow, Color.Success),
Status.Paused => (Icons.Material.Filled.Pause, Color.Default),
Status.Stopped => (Icons.Material.Filled.Stop, Color.Default),
Status.Active => (Icons.Material.Filled.Sort, Color.Success),
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
@@ -417,5 +416,25 @@ namespace Lantean.QBTMud.Helpers
_ => (Icons.Material.Filled.QuestionMark, Color.Inherit),
};
}
public static string Bool(bool value, string trueText = "Yes", string falseText = "No")
{
return value ? trueText : falseText;
}
public static string RatioLimit(float value)
{
if (value == Limits.GlobalLimit)
{
return "Global";
}
if (value <= Limits.NoLimit)
{
return "∞";
}
return value.ToString("0.00");
}
}
}
}

View File

@@ -0,0 +1,40 @@
using Microsoft.AspNetCore.Components.Web;
namespace Lantean.QBTMud.Helpers
{
public static class EventArgsExtensions
{
public static EventArgs NormalizeForContextMenu(this EventArgs eventArgs)
{
ArgumentNullException.ThrowIfNull(eventArgs);
if (eventArgs is LongPressEventArgs longPressEventArgs)
{
return longPressEventArgs.ToMouseEventArgs();
}
return eventArgs;
}
public static MouseEventArgs ToMouseEventArgs(this LongPressEventArgs longPressEventArgs)
{
ArgumentNullException.ThrowIfNull(longPressEventArgs);
return new MouseEventArgs
{
Button = 2,
Buttons = 2,
ClientX = longPressEventArgs.ClientX,
ClientY = longPressEventArgs.ClientY,
OffsetX = longPressEventArgs.OffsetX,
OffsetY = longPressEventArgs.OffsetY,
PageX = longPressEventArgs.PageX,
PageY = longPressEventArgs.PageY,
ScreenX = longPressEventArgs.ScreenX,
ScreenY = longPressEventArgs.ScreenY,
Type = longPressEventArgs.Type ?? "contextmenu",
Detail = -1,
};
}
}
}

View File

@@ -119,34 +119,35 @@ namespace Lantean.QBTMud.Helpers
switch (category)
{
case CATEGORY_ALL:
break;
return true;
case CATEGORY_UNCATEGORIZED:
if (!string.IsNullOrEmpty(torrent.Category))
{
return false;
}
break;
return true;
default:
if (string.IsNullOrEmpty(torrent.Category))
{
return false;
}
if (!useSubcategories)
{
if (torrent.Category != category)
{
return false;
}
else
{
if (!torrent.Category.StartsWith(category))
{
return false;
}
}
return string.Equals(torrent.Category, category, StringComparison.Ordinal);
}
break;
}
return true;
if (string.Equals(torrent.Category, category, StringComparison.Ordinal))
{
return true;
}
var prefix = string.Concat(category, "/");
return torrent.Category.StartsWith(prefix, StringComparison.Ordinal);
}
}
public static bool FilterTag(Torrent torrent, string tag)
@@ -199,15 +200,8 @@ namespace Lantean.QBTMud.Helpers
break;
case Status.Resumed:
if (!state.Contains("resumed"))
{
return false;
}
break;
case Status.Paused:
if (!state.Contains("paused") || !state.Contains("stopped"))
case Status.Stopped:
if (state != "stoppedDL" && state != "stoppedUP")
{
return false;
}
@@ -284,4 +278,4 @@ namespace Lantean.QBTMud.Helpers
};
}
}
}
}

View File

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

namespace Lantean.QBTMud.Helpers
{
internal static class VersionHelper
{
private static int? _version;
private const int _defaultVersion = 5;
public static int DefaultVersion => _defaultVersion;
public static int GetMajorVersion(string? version)
{
if (_version is not null)
{
return _version.Value;
}
if (string.IsNullOrEmpty(version))
{
return _defaultVersion;
}
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
{
return _defaultVersion;
}
_version = theVersion.Major;
return _version.Value;
}
}
}

View File

@@ -12,10 +12,10 @@
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="MudBlazor" Version="8.7.0" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="MudBlazor" Version="8.13.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
</ItemGroup>

View File

@@ -1,9 +1,11 @@
@inherits LayoutComponentBase
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
</MudDrawer>
<MudMainContent>
@Body
</MudMainContent>
<div class="app-shell__body">
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
</MudDrawer>
<MudMainContent Class="app-shell__main">
@Body
</MudMainContent>
</div>

View File

@@ -1,11 +1,13 @@
@inherits LayoutComponentBase
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
</MudDrawer>
<MudMainContent>
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
@Body
</CascadingValue>
</MudMainContent>
<div class="app-shell__body">
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
</MudDrawer>
<MudMainContent Class="app-shell__main">
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
@Body
</CascadingValue>
</MudMainContent>
</div>

View File

@@ -10,20 +10,61 @@
}
<CascadingValue Value="Torrents">
<CascadingValue Value="MainData">
<CascadingValue Value="Preferences">
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
<CascadingValue Value="SortColumn" Name="SortColumn">
<CascadingValue Value="SortDirectionChanged" Name="SortDirectionChanged">
<CascadingValue Value="SortDirection" Name="SortDirection">
<CascadingValue Value="CategoryChanged" Name="CategoryChanged">
<CascadingValue Value="StatusChanged" Name="StatusChanged">
<CascadingValue Value="TagChanged" Name="TagChanged">
<CascadingValue Value="TrackerChanged" Name="TrackerChanged">
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
<CascadingValue Value="Version" Name="Version">
@Body
<CascadingValue Value="_torrentsVersion" Name="TorrentsVersion">
<CascadingValue Value="MainData">
<CascadingValue Value="Preferences">
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
<CascadingValue Value="SortColumn" Name="SortColumn">
<CascadingValue Value="SortDirectionChanged" Name="SortDirectionChanged">
<CascadingValue Value="SortDirection" Name="SortDirection">
<CascadingValue Value="CategoryChanged" Name="CategoryChanged">
<CascadingValue Value="StatusChanged" Name="StatusChanged">
<CascadingValue Value="TagChanged" Name="TagChanged">
<CascadingValue Value="TrackerChanged" Name="TrackerChanged">
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
<CascadingValue Value="@(MainData?.LostConnection ?? false)" Name="LostConnection">
<CascadingValue Value="Version" Name="Version">
<div class="app-shell">
@Body
<MudAppBar Bottom="true" Elevation="0" Dense="true" Class="app-shell__status-bar">
@if (MainData?.LostConnection == true)
{
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
@{
var externalIpLabel = Preferences?.StatusBarExternalIp == true ? BuildExternalIpLabel(MainData?.ServerState) : null;
}
@if (!string.IsNullOrEmpty(externalIpLabel))
{
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@externalIpLabel</MudText>
}
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="@MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
</MudText>
<MudDivider Vertical="true" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Info" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
</MudText>
</MudAppBar>
</div>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
@@ -36,34 +77,5 @@
</CascadingValue>
</CascadingValue>
</CascadingValue>
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
@if (MainData?.LostConnection == true)
{
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
</MudText>
<MudDivider Vertical="true" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Info" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
</MudText>
</MudAppBar>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@@ -52,22 +52,36 @@ namespace Lantean.QBTMud.Layout
protected string? SearchText { get; set; }
protected IEnumerable<Torrent> Torrents => GetTorrents();
protected IReadOnlyList<Torrent> Torrents => GetTorrents();
protected bool IsAuthenticated { get; set; }
protected bool LostConnection { get; set; }
private List<Torrent> GetTorrents()
private IReadOnlyList<Torrent> _visibleTorrents = Array.Empty<Torrent>();
private bool _torrentsDirty = true;
private int _torrentsVersion;
private IReadOnlyList<Torrent> GetTorrents()
{
if (!_torrentsDirty)
{
return _visibleTorrents;
}
if (MainData is null)
{
return [];
_visibleTorrents = Array.Empty<Torrent>();
_torrentsDirty = false;
return _visibleTorrents;
}
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
_visibleTorrents = MainData.Torrents.Values.Filter(filterState).ToList();
_torrentsDirty = false;
return MainData.Torrents.Values.Filter(filterState).ToList();
return _visibleTorrents;
}
protected override async Task OnInitializedAsync()
@@ -83,7 +97,8 @@ namespace Lantean.QBTMud.Layout
Preferences = await ApiClient.GetApplicationPreferences();
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
MainData = DataManager.CreateMainData(data, Version);
MainData = DataManager.CreateMainData(data);
MarkTorrentsDirty();
_requestId = data.ResponseId;
_refreshInterval = MainData.ServerState.RefreshInterval;
@@ -126,32 +141,51 @@ namespace Lantean.QBTMud.Layout
return;
}
var shouldRender = false;
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data, Version);
MainData = DataManager.CreateMainData(data);
MarkTorrentsDirty();
shouldRender = true;
}
else
{
DataManager.MergeMainData(data, MainData);
var dataChanged = DataManager.MergeMainData(data, MainData, out var filterChanged);
if (filterChanged)
{
MarkTorrentsDirty();
}
else if (dataChanged)
{
IncrementTorrentsVersion();
}
shouldRender = dataChanged;
}
_refreshInterval = MainData.ServerState.RefreshInterval;
if (MainData is not null)
{
_refreshInterval = MainData.ServerState.RefreshInterval;
}
_requestId = data.ResponseId;
await InvokeAsync(StateHasChanged);
if (shouldRender)
{
await InvokeAsync(StateHasChanged);
}
}
}
}
}
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, OnCategoryChanged);
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, OnStatusChanged);
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, OnTagChanged);
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, OnTrackerChanged);
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, OnSearchTermChanged);
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
@@ -159,12 +193,107 @@ namespace Lantean.QBTMud.Layout
protected static (string, Color) GetConnectionIcon(string? status)
{
if (status is null)
return status switch
{
return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
"firewalled" => (Icons.Material.Outlined.SignalWifiStatusbarConnectedNoInternet4, Color.Warning),
"connected" => (Icons.Material.Outlined.SignalWifi4Bar, Color.Success),
_ => (Icons.Material.Outlined.SignalWifiOff, Color.Error),
};
}
private static string? BuildExternalIpLabel(ServerState? serverState)
{
if (serverState is null)
{
return null;
}
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
var v4 = serverState.LastExternalAddressV4;
var v6 = serverState.LastExternalAddressV6;
var hasV4 = !string.IsNullOrWhiteSpace(v4);
var hasV6 = !string.IsNullOrWhiteSpace(v6);
if (!hasV4 && !hasV6)
{
return "External IP: N/A";
}
if (hasV4 && hasV6)
{
return $"External IPs: {v4}, {v6}";
}
var address = hasV4 ? v4 : v6;
return $"External IP: {address}";
}
private void OnCategoryChanged(string category)
{
if (Category == category)
{
return;
}
Category = category;
MarkTorrentsDirty();
}
private void OnStatusChanged(Status status)
{
if (Status == status)
{
return;
}
Status = status;
MarkTorrentsDirty();
}
private void OnTagChanged(string tag)
{
if (Tag == tag)
{
return;
}
Tag = tag;
MarkTorrentsDirty();
}
private void OnTrackerChanged(string tracker)
{
if (Tracker == tracker)
{
return;
}
Tracker = tracker;
MarkTorrentsDirty();
}
private void OnSearchTermChanged(string term)
{
if (SearchText == term)
{
return;
}
SearchText = term;
MarkTorrentsDirty();
}
private void MarkTorrentsDirty()
{
_torrentsDirty = true;
IncrementTorrentsVersion();
}
private void IncrementTorrentsVersion()
{
unchecked
{
_torrentsVersion++;
}
}
protected virtual void Dispose(bool disposing)
@@ -188,4 +317,4 @@ namespace Lantean.QBTMud.Layout
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -1,11 +1,13 @@
@inherits LayoutComponentBase
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<MudNavMenu>
<ApplicationActions IsMenu="false" Preferences="Preferences" />
</MudNavMenu>
</MudDrawer>
<MudMainContent>
@Body
</MudMainContent>
<div class="app-shell__body">
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false" Class="app-shell__sidebar">
<MudNavMenu>
<ApplicationActions IsMenu="false" Preferences="Preferences" />
</MudNavMenu>
</MudDrawer>
<MudMainContent Class="app-shell__main">
@Body
</MudMainContent>
</div>

View File

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

View File

@@ -27,7 +27,17 @@
long uploadRateLimit,
bool useAltSpeedLimits,
bool useSubcategories,
float writeCacheOverload) : base(connectionStatus, dHTNodes, downloadInfoData, downloadInfoSpeed, downloadRateLimit, uploadInfoData, uploadInfoSpeed, uploadRateLimit)
float writeCacheOverload,
string lastExternalAddressV4,
string lastExternalAddressV6) : base(
connectionStatus,
dHTNodes,
downloadInfoData,
downloadInfoSpeed,
downloadRateLimit,
uploadInfoData,
uploadInfoSpeed,
uploadRateLimit)
{
AllTimeDownloaded = allTimeDownloaded;
AllTimeUploaded = allTimeUploaded;
@@ -46,6 +56,8 @@
UseAltSpeedLimits = useAltSpeedLimits;
UseSubcategories = useSubcategories;
WriteCacheOverload = writeCacheOverload;
LastExternalAddressV4 = lastExternalAddressV4;
LastExternalAddressV6 = lastExternalAddressV6;
}
public ServerState()
@@ -85,5 +97,9 @@
public bool UseSubcategories { get; set; }
public float WriteCacheOverload { get; set; }
public string LastExternalAddressV4 { get; set; } = string.Empty;
public string LastExternalAddressV6 { get; set; } = string.Empty;
}
}
}

View File

@@ -1,10 +1,13 @@
namespace Lantean.QBTMud.Models
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMud.Models
{
public record ShareRatio
{
public float RatioLimit { get; set; }
public float SeedingTimeLimit { get; set; }
public float InactiveSeedingTimeLimit { get; set; }
public ShareLimitAction? ShareLimitAction { get; set; }
}
public record ShareRatioMax : ShareRatio
@@ -13,4 +16,4 @@
public float MaxSeedingTime { get; set; }
public float MaxInactiveSeedingTime { get; set; }
}
}
}

View File

@@ -6,8 +6,6 @@
Downloading,
Seeding,
Completed,
Resumed,
Paused,
Stopped,
Active,
Inactive,
@@ -16,6 +14,5 @@
StalledDownloading,
Checking,
Errored,
}
}
}

View File

@@ -1,4 +1,9 @@
namespace Lantean.QBTMud.Models
using System;
using System.Collections.Generic;
using System.Linq;
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMud.Models
{
public class Torrent
{
@@ -52,7 +57,13 @@
long uploadSpeed,
long reannounce,
float inactiveSeedingTimeLimit,
float maxInactiveSeedingTime)
float maxInactiveSeedingTime,
float popularity,
string downloadPath,
string rootPath,
bool isPrivate,
ShareLimitAction shareLimitAction,
string comment)
{
Hash = hash;
AddedOn = addedOn;
@@ -104,21 +115,31 @@
Reannounce = reannounce;
InactiveSeedingTimeLimit = inactiveSeedingTimeLimit;
MaxInactiveSeedingTime = maxInactiveSeedingTime;
Popularity = popularity;
DownloadPath = downloadPath;
RootPath = rootPath;
IsPrivate = isPrivate;
ShareLimitAction = shareLimitAction;
Comment = comment;
}
protected Torrent()
{
Hash = "";
Category = "";
ContentPath = "";
InfoHashV1 = "";
InfoHashV2 = "";
MagnetUri = "";
Name = "";
SavePath = "";
State = "";
Tags = [];
Tracker = "";
Hash = string.Empty;
Category = string.Empty;
ContentPath = string.Empty;
InfoHashV1 = string.Empty;
InfoHashV2 = string.Empty;
MagnetUri = string.Empty;
Name = string.Empty;
SavePath = string.Empty;
DownloadPath = string.Empty;
RootPath = string.Empty;
State = string.Empty;
Tags = new List<string>();
Tracker = string.Empty;
ShareLimitAction = ShareLimitAction.Default;
Comment = string.Empty;
}
public string Hash { get; }
@@ -183,8 +204,14 @@
public float RatioLimit { get; set; }
public float Popularity { get; set; }
public string SavePath { get; set; }
public string DownloadPath { get; set; }
public string RootPath { get; set; }
public long SeedingTime { get; set; }
public int SeedingTimeLimit { get; set; }
@@ -221,6 +248,12 @@
public float MaxInactiveSeedingTime { get; set; }
public bool IsPrivate { get; set; }
public ShareLimitAction ShareLimitAction { get; set; }
public string Comment { get; set; }
public override bool Equals(object? obj)
{
if (obj is null)
@@ -241,4 +274,4 @@
return Hash;
}
}
}
}

View File

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

View File

@@ -1,18 +1,22 @@
@page "/about"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">About</MudText>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">About</MudText>
</MudToolBar>
</div>
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
<MudTabPanel Text="About">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<div class="content-panel__body">
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
<MudTabPanel Text="About">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 content-panel__container options-tab-contents">
<MudGrid Class="mt-0 mb-4">
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
@@ -60,7 +64,7 @@
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Authors">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
<MudGrid Class="mt-0 mb-4">
@@ -108,7 +112,7 @@
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Special Thanks">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
<MudText Typo="Typo.body1" Class="py-1">I would first like to thank sourceforge.net for hosting qBittorrent project and for their support.</MudText>
<MudText Typo="Typo.body1" Class="py-1">I am pleased that people from all over the world are contributing to qBittorrent: Ishan Arora (India), Arnaud Demaizière (France) and Stephanos Antaris (Greece). Their help is greatly appreciated</MudText>
<MudText Typo="Typo.body1" Class="py-1">I also want to thank Στέφανος Αντάρης (santaris@csd.auth.gr) and Mirco Chinelli (infinity89@fastwebmail.it) for working on Mac OS X packaging.</MudText>
@@ -118,7 +122,7 @@
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Translators">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
<MudText Typo="Typo.body1" Class="py-1">
I would like to thank the people who volunteered to Circle qBittorrent.<br>
Most of them Circled via <MudLink Target="https://www.transifex.com/sledgehammer999/qbittorrent/" Href="https://www.transifex.com/sledgehammer999/qbittorrent/">Transifex</MudLink> and some of them are mentioned below:<br>
@@ -168,7 +172,7 @@
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Licence">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 options-tab-contents">
<MudText Typo="Typo.body1" Class="py-1">
The qBittorrent source code is licensed under the GNU General Public License, version 2 or (at your option) any later version (GPLv2+).
However, this binary distribution is licensed under GNU General Public License, version 3 or (at your option) any later version (GPLv3+),
@@ -1061,7 +1065,7 @@
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Software Used">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3 mb-3 options-tab-contents">
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
<MudGrid Class="mt-1 mb-4">
@@ -1104,4 +1108,6 @@
<MudText Typo="Typo.body1" Class="py-1">The free IP to Country Lite database by DB-IP is used for resolving the countries of peers. The database is licensed under the Creative Commons Attribution 4.0 International License (<MudLink Target="https://db-ip.com/" Href="https://db-ip.com/" rel="noopener ">https://db-ip.com/</MudLink>)</MudText>
</MudContainer>
</MudTabPanel>
</MudTabs>
</MudTabs>
</div>
</div>

View File

@@ -1,36 +1,41 @@
@page "/blocks"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
</MudToolBar>
</div>
<div class="content-panel__body">
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="10">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="10">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.PeerLog"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
RowClassFunc="RowClass"
Class="search-list" />
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.PeerLog"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
RowClassFunc="RowClass"
Class="search-list content-panel__table" />
</div>
</div>

View File

@@ -1,24 +1,30 @@
@page "/categories"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Categories</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Categories</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Filled.PlaylistAdd" OnClick="AddCategory" title="Add Category" />
</MudToolBar>
</div>
<DynamicTable @ref="Table"
T="Category"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list" />
<div class="content-panel__body">
<DynamicTable @ref="Table"
T="Category"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list content-panel__table" />
</div>
</div>
@code {
private RenderFragment<RowContext<Category>> ActionsColumn

View File

@@ -1,41 +1,45 @@
@page "/details/{hash}"
@layout DetailsLayout
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
@if (Hash is not null)
{
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>
</MudToolBar>
</div>
<div class="content-panel">
<div class="content-panel__toolbar content-panel__toolbar--scroll">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
@if (Hash is not null)
{
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" Torrents="MainData.Torrents" Preferences="Preferences" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>
</MudToolBar>
</div>
@if (ShowTabs)
{
<CascadingValue Value="RefreshInterval" Name="RefreshInterval">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
<MudTabPanel Text="General">
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
</MudTabPanel>
<MudTabPanel Text="Trackers">
<TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
</MudTabPanel>
<MudTabPanel Text="Peers">
<PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
</MudTabPanel>
<MudTabPanel Text="HTTP Sources">
<WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
</MudTabPanel>
<MudTabPanel Text="Content">
<FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
</MudTabPanel>
</MudTabs>
</CascadingValue>
}
<div class="content-panel__body">
@if (ShowTabs)
{
<CascadingValue Value="RefreshInterval" Name="RefreshInterval">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
<MudTabPanel Text="General">
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
</MudTabPanel>
<MudTabPanel Text="Trackers">
<TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
</MudTabPanel>
<MudTabPanel Text="Peers">
<PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
</MudTabPanel>
<MudTabPanel Text="HTTP Sources">
<WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
</MudTabPanel>
<MudTabPanel Text="Content">
<FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
</MudTabPanel>
</MudTabs>
</CascadingValue>
}
</div>
</div>

View File

@@ -1,44 +1,49 @@
@page "/log"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Execution Log</MudText>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Execution Log</MudText>
</MudToolBar>
</div>
<div class="content-panel__body">
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="7">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
</MudItem>
<MudItem md="3">
<MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
<MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
<MudSelectItem Value="@("Info")">Info</MudSelectItem>
<MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
<MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="7">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" Variant="Variant.Outlined" />
</MudItem>
<MudItem md="3">
<MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
<MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
<MudSelectItem Value="@("Info")">Info</MudSelectItem>
<MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
<MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.Log"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
RowClassFunc="RowClass"
Class="search-list" />
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.Log"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
RowClassFunc="RowClass"
Class="search-list content-panel__table" />
</div>
</div>

View File

@@ -3,41 +3,63 @@
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Settings</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
<MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Settings</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Save" OnClick="Save" Disabled="@(LostConnection || UpdatePreferences is null)" />
<MudIconButton Icon="@Icons.Material.Outlined.Undo" OnClick="Undo" Disabled="@(LostConnection || UpdatePreferences is null)" />
</MudToolBar>
</div>
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
<MudTabPanel Text="Behaviour">
<BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="Downloads">
<DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="Connection">
<ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="Speed">
<SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="BitTorrent">
<BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="RSS">
<RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="Web UI">
<WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
<MudTabPanel Text="Advanced">
<AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</MudTabPanel>
</MudTabs>
<div class="content-panel__body">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" Border="true">
<MudTabPanel Text="Behaviour">
<div class="options-tab-contents">
<BehaviourOptions @ref="BehaviourOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="Downloads">
<div class="options-tab-contents">
<DownloadsOptions @ref="DownloadsOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="Connection">
<div class="options-tab-contents">
<ConnectionOptions @ref="ConnectionOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="Speed">
<div class="options-tab-contents">
<SpeedOptions @ref="SpeedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="BitTorrent">
<div class="options-tab-contents">
<BitTorrentOptions @ref="BitTorrentOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="RSS">
<div class="options-tab-contents">
<RSSOptions @ref="RSSOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="Web UI">
<div class="options-tab-contents">
<WebUIOptions @ref="WebUIOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
<MudTabPanel Text="Advanced">
<div class="options-tab-contents">
<AdvancedOptions @ref="AdvancedOptions" Preferences="Preferences" UpdatePreferences="@UpdatePreferences" PreferencesChanged="PreferencesChanged" />
</div>
</MudTabPanel>
</MudTabs>
</div>
</div>

View File

@@ -1,73 +1,79 @@
@page "/rss"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">RSS</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
<MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" Disabled="@(SelectedFeed is null)" title="Mark items read" />
<MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
</MudToolBar>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
<MudGrid Class="rss-contents">
<MudItem xs="4" Style="height: 100%">
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
<MudListItem Icon="@Icons.Material.Filled.MarkEmailUnread" Text="@($"Unread ({UnreadCount})")" Value="@("unread")" />
@foreach (var (key, feed) in Feeds)
{
<MudListItem Icon="@(feed.IsLoading ? Icons.Material.Filled.Sync : Icons.Material.Filled.Wifi)" Class="@(feed.IsLoading ? "spin-animation" : "")" Text="@($"{feed.Title} ({feed.UnreadCount})")" Value="@key" />
}
</MudList>
</MudItem>
<MudItem xs="4" Style="height: 100%; overflow: auto">
@if (Articles.Count > 0)
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged" Dense>
@foreach (var article in Articles)
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">RSS</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
<MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" Disabled="@(SelectedFeed is null)" title="Mark items read" />
<MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
</MudToolBar>
</div>
<div class="content-panel__body">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="content-panel__container">
<MudGrid Class="rss-contents">
<MudItem xs="4" Style="height: 100%">
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged" Dense>
<MudListItem Icon="@Icons.Material.Filled.MarkEmailUnread" Text="@($"Unread ({UnreadCount})")" Value="@("unread")" />
@foreach (var (key, feed) in Feeds)
{
<MudListItem Icon="@(feed.IsLoading ? Icons.Material.Filled.Sync : Icons.Material.Filled.Wifi)" Class="@(feed.IsLoading ? "spin-animation" : "")" Text="@($"{feed.Title} ({feed.UnreadCount})")" Value="@key" />
}
</MudList>
</MudItem>
<MudItem xs="4" Style="height: 100%; overflow: auto">
@if (Articles.Count > 0)
{
<MudListItem Text="@article.Title" Value="article.Id" Icon="@Icons.Material.Filled.Check" IconColor="@(article.IsRead ? Color.Success : Color.Transparent)" />
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged" Dense>
@foreach (var article in Articles)
{
<MudListItem Text="@article.Title" Value="article.Id" Icon="@Icons.Material.Filled.Check" IconColor="@(article.IsRead ? Color.Success : Color.Transparent)" />
}
</MudList>
}
</MudList>
}
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
<MudItem xs="4" Style="height: 100%">
@if (Article is not null)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6" Style="overflow-wrap: anywhere">@Article.Title</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
<MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(Article.TorrentURL)" title="Download">Download</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@Article.TorrentURL" Target="@Article.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
</MudMenu>
</CardHeaderActions>
</MudCardHeader>
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
<MudItem xs="4" Style="height: 100%">
@if (Article is not null)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6" Style="overflow-wrap: anywhere">@Article.Title</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
<MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(Article.TorrentURL)" title="Download">Download</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@Article.TorrentURL" Target="@Article.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
</MudMenu>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.subtitle2">@Article.Date</MudText>
<MudText Typo="Typo.body1">@Article.Description</MudText>
</MudCardContent>
</MudCard>
}
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
</MudGrid>
</MudContainer>
<MudCardContent>
<MudText Typo="Typo.subtitle2">@Article.Date</MudText>
<MudText Typo="Typo.body1">@Article.Description</MudText>
</MudCardContent>
</MudCard>
}
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
</MudGrid>
</MudContainer>
</div>
</div>

View File

@@ -1,62 +1,67 @@
@page "/search"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Search</MudText>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Search</MudText>
</MudToolBar>
</div>
<div class="content-panel__body">
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnValidSubmit="DoSearch">
<MudGrid>
<MudItem xs="12" md="4">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
@foreach (var (value, name) in Categories)
{
<MudSelectItem Value="value">@name</MudSelectItem>
if (value == "all")
{
<MudDivider />
}
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
<MudSelectItem Value="@("all")">All</MudSelectItem>
@if (Plugins.Count > 0)
{
<MudDivider />
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<EditForm Model="Model" OnValidSubmit="DoSearch">
<MudGrid>
<MudItem xs="12" md="4">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
@foreach (var (value, name) in Categories)
{
<MudSelectItem Value="value">@name</MudSelectItem>
if (value == "all")
{
<MudDivider />
}
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
<MudSelectItem Value="@("all")">All</MudSelectItem>
@if (Plugins.Count > 0)
{
<MudDivider />
}
@foreach (var (value, name) in Plugins)
{
<MudSelectItem Value="value">@name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
}
@foreach (var (value, name) in Plugins)
{
<MudSelectItem Value="value">@name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12" md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.SearchResult"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="search-list" />
<DynamicTable @ref="Table"
T="Lantean.QBitTorrentClient.Models.SearchResult"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="search-list content-panel__table" />
</div>
</div>

View File

@@ -1,62 +1,68 @@
@page "/statistics"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Statistics</MudText>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Statistics</MudText>
</MudToolBar>
</div>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents">
<MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="All-time uploaded">@DisplayHelpers.Size(ServerState?.AllTimeUploaded)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="All-time downloaded">@DisplayHelpers.Size(ServerState?.AllTimeDownloaded)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="All-time share ratio">@DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00")</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Session waste">@DisplayHelpers.Size(ServerState?.TotalWastedSession)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Connected peers">@DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections)</MudField>
</MudItem>
</MudGrid>
<div class="content-panel__body">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents content-panel__container">
<MudText Typo="Typo.subtitle2" Class="pt-6">User statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="All-time uploaded">@DisplayHelpers.Size(ServerState?.AllTimeUploaded)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="All-time downloaded">@DisplayHelpers.Size(ServerState?.AllTimeDownloaded)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="All-time share ratio">@DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00")</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Session waste">@DisplayHelpers.Size(ServerState?.TotalWastedSession)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Connected peers">@DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections)</MudField>
</MudItem>
</MudGrid>
<MudText Typo="Typo.subtitle2" Class="pt-6">Cache statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="Read cache hits">@DisplayHelpers.Percentage(ServerState?.ReadCacheHits)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Total buffer size">@DisplayHelpers.Size(ServerState?.TotalBuffersSize)</MudField>
</MudItem>
</MudGrid>
<MudText Typo="Typo.subtitle2" Class="pt-6">Cache statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="Read cache hits">@DisplayHelpers.Percentage(ServerState?.ReadCacheHits)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Total buffer size">@DisplayHelpers.Size(ServerState?.TotalBuffersSize)</MudField>
</MudItem>
</MudGrid>
<MudText Typo="Typo.subtitle2" Class="pt-6">Performance statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="Write cache overload">@DisplayHelpers.Percentage(ServerState?.WriteCacheOverload)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Read cache overload">@DisplayHelpers.Percentage(ServerState?.ReadCacheOverload)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Queued I/O jobs">@DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Average time in queue">@DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms")</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField>
</MudItem>
</MudGrid>
</MudContainer>
<MudText Typo="Typo.subtitle2" Class="pt-6">Performance statistics</MudText>
<MudGrid>
<MudItem xs="12">
<MudField Label="Write cache overload">@DisplayHelpers.Percentage(ServerState?.WriteCacheOverload)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Read cache overload">@DisplayHelpers.Percentage(ServerState?.ReadCacheOverload)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Queued I/O jobs">@DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Average time in queue">@DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms")</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Total queued size">@DisplayHelpers.Size(ServerState?.TotalQueuedSize)</MudField>
</MudItem>
</MudGrid>
</MudContainer>
</div>
</div>

View File

@@ -1,24 +1,30 @@
@page "/tags"
@layout OtherLayout
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Tags</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" />
}
<MudText Class="px-5 no-wrap">Tags</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Filled.NewLabel" OnClick="AddTag" title="Add Tag" />
</MudToolBar>
</div>
<DynamicTable @ref="Table"
T="string"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list" />
<div class="content-panel__body">
<DynamicTable @ref="Table"
T="string"
ColumnDefinitions="Columns"
Items="Results"
MultiSelection="false"
SelectOnRowClick="false"
Class="details-list content-panel__table" />
</div>
</div>
@code {
private RenderFragment<RowContext<string>> ActionsColumn

View File

@@ -1,44 +1,47 @@
@page "/"
@layout ListLayout
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
<MudMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" PositionAtCursor="true" ListClass="unselectable" PopoverClass="unselectable">
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
<MudDivider />
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
</ContextMenu>
</MudMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
<MudDivider Vertical="true" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrentToolbar" title="View torrent details" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudSpacer />
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
<div class="content-panel">
<div class="content-panel__toolbar content-panel__toolbar--scroll">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
<MudDivider Vertical="true" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrentsHashes()" Torrents="MainData.Torrents" Preferences="Preferences" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrentToolbar" title="View torrent details" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudSpacer />
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
</div>
<div class="content-panel__body">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0 content-panel__container">
<DynamicTable
@ref="Table"
T="Torrent"
Class="torrent-list content-panel__table"
ColumnDefinitions="Columns"
Items="Torrents"
OnRowClick="RowClick"
MultiSelection="true"
SelectOnRowClick="true"
SelectedItemsChanged="SelectedItemsChanged"
SortColumnChanged="SortColumnChangedHandler"
SortDirectionChanged="SortDirectionChangedHandler"
OnTableDataContextMenu="TableDataContextMenu"
OnTableDataLongPress="TableDataLongPress"
/>
</MudContainer>
</div>
</div>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable
@ref="Table"
T="Torrent"
Class="torrent-list"
ColumnDefinitions="Columns"
Items="Torrents"
OnRowClick="RowClick"
MultiSelection="true"
SelectOnRowClick="true"
SelectedItemsChanged="SelectedItemsChanged"
SortColumnChanged="SortColumnChangedHandler"
SortDirectionChanged="SortDirectionChangedHandler"
OnTableDataContextMenu="TableDataContextMenu"
OnTableDataLongPress="TableDataLongPress"
/>
</MudContainer>
@code {
private static RenderFragment<RowContext<Torrent>> ProgressBarColumn
{

View File

@@ -35,11 +35,17 @@ namespace Lantean.QBTMud.Pages
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[CascadingParameter]
public IEnumerable<Torrent>? Torrents { get; set; }
public IReadOnlyList<Torrent>? Torrents { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
[CascadingParameter(Name = "LostConnection")]
public bool LostConnection { get; set; }
[CascadingParameter(Name = "TorrentsVersion")]
public int TorrentsVersion { get; set; }
[CascadingParameter(Name = "SearchTermChanged")]
public EventCallback<string> SearchTermChanged { get; set; }
@@ -56,13 +62,23 @@ namespace Lantean.QBTMud.Pages
protected HashSet<Torrent> SelectedItems { get; set; } = [];
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
protected bool ToolbarButtonsEnabled => _toolbarButtonsEnabled;
protected DynamicTable<Torrent>? Table { get; set; }
protected Torrent? ContextMenuItem { get; set; }
protected ContextMenu? ContextMenu { get; set; }
protected MudMenu? ContextMenu { get; set; }
private object? _lastRenderedTorrents;
private QBitTorrentClient.Models.Preferences? _lastPreferences;
private bool _lastLostConnection;
private bool _hasRendered;
private int _lastSelectionCount;
private int _lastTorrentsVersion = -1;
private bool _pendingSelectionChange;
private bool _toolbarButtonsEnabled;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
@@ -73,9 +89,81 @@ namespace Lantean.QBTMud.Pages
}
}
protected override bool ShouldRender()
{
if (!_hasRendered)
{
_hasRendered = true;
_lastRenderedTorrents = Torrents;
_lastPreferences = Preferences;
_lastLostConnection = LostConnection;
_lastTorrentsVersion = TorrentsVersion;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (_pendingSelectionChange)
{
_pendingSelectionChange = false;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (_lastTorrentsVersion != TorrentsVersion)
{
_lastTorrentsVersion = TorrentsVersion;
_lastRenderedTorrents = Torrents;
_lastPreferences = Preferences;
_lastLostConnection = LostConnection;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (!ReferenceEquals(_lastRenderedTorrents, Torrents))
{
_lastRenderedTorrents = Torrents;
_lastPreferences = Preferences;
_lastLostConnection = LostConnection;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (!ReferenceEquals(_lastPreferences, Preferences))
{
_lastPreferences = Preferences;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (_lastLostConnection != LostConnection)
{
_lastLostConnection = LostConnection;
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
if (_lastSelectionCount != SelectedItems.Count)
{
_lastSelectionCount = SelectedItems.Count;
_toolbarButtonsEnabled = _lastSelectionCount > 0;
return true;
}
return false;
}
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
{
SelectedItems = selectedItems;
_toolbarButtonsEnabled = SelectedItems.Count > 0;
_pendingSelectionChange = true;
InvokeAsync(StateHasChanged);
}
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
@@ -185,7 +273,9 @@ namespace Lantean.QBTMud.Pages
return;
}
await ContextMenu.ToggleMenuAsync(eventArgs);
var normalizedEventArgs = eventArgs.NormalizeForContextMenu();
await ContextMenu.OpenMenuAsync(normalizedEventArgs);
}
protected IEnumerable<ColumnDefinition<Torrent>> Columns => ColumnsDefinitions.Where(c => c.Id != "#" || Preferences?.QueueingEnabled == true);
@@ -205,6 +295,7 @@ namespace Lantean.QBTMud.Pages
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Up Speed", t => t.UploadSpeed, t => DisplayHelpers.Speed(t.UploadSpeed)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("ETA", t => t.EstimatedTimeOfArrival, t => DisplayHelpers.Duration(t.EstimatedTimeOfArrival)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio", t => t.Ratio, t => t.Ratio.ToString("0.00")),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Popularity", t => t.Popularity, t => t.Popularity.ToString("0.00")),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Category", t => t.Category),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Tags", t => t.Tags, t => string.Join(", ", t.Tags)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Added On", t => t.AddedOn, t => DisplayHelpers.DateTime(t.AddedOn)),
@@ -220,11 +311,15 @@ namespace Lantean.QBTMud.Pages
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Save path", t => t.SavePath, enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Completed", t => t.Completed, t => DisplayHelpers.Size(t.Completed), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => t.Ratio.ToString("0.00"), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Ratio Limit", t => t.RatioLimit, t => DisplayHelpers.RatioLimit(t.RatioLimit), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Availability", t => t.Availability, t => t.Availability.ToString("0.##"), enabled: false),
//ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Incomplete Save Path", t => t.DownloadPath, t => DisplayHelpers.EmptyIfNull(t.DownloadPath), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v1", t => t.InfoHashV1, t => DisplayHelpers.EmptyIfNull(t.InfoHashV1), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Info Hash v2", t => t.InfoHashV2, t => DisplayHelpers.EmptyIfNull(t.InfoHashV2), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Reannounce In", t => t.Reannounce, t => DisplayHelpers.Duration(t.Reannounce), enabled: false),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Private", t => t.IsPrivate, t => DisplayHelpers.Bool(t.IsPrivate), enabled: false),
];
public async ValueTask DisposeAsync()
@@ -248,4 +343,4 @@ namespace Lantean.QBTMud.Pages
}
}
}
}
}

View File

@@ -1,4 +1,4 @@
using Blazored.LocalStorage;
using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components.Web;

File diff suppressed because it is too large Load Diff

View File

@@ -4,11 +4,11 @@ namespace Lantean.QBTMud.Services
{
public interface IDataManager
{
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList);
bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
@@ -16,7 +16,7 @@ namespace Lantean.QBTMud.Services
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);

View File

@@ -65,15 +65,11 @@ code {
}
.mud-appbar.mud-appbar-fixed-bottom {
height: 35px;
}
.mud-main-content {
padding-bottom: 35px;
height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
}
.mud-drawer-fixed.mud-drawer-mini.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-persistent:not(.mud-drawer-clipped-never), .mud-drawer-fixed.mud-drawer-responsive.mud-drawer-clipped-always, .mud-drawer-fixed.mud-drawer-temporary.mud-drawer-clipped-always {
height: calc(100% - var(--mud-appbar-height) - 35px);
height: calc(100% - var(--mud-appbar-height) - (var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px)));
}
.w-100 {
@@ -154,25 +150,91 @@ code {
margin-right: 5px;
}
.torrent-list .mud-table-container {
height: calc(100vh - 160px);
/*. Layout helpers */
.content-panel {
display: flex;
flex-direction: column;
height: 100%;
min-height: 0;
}
.file-list .mud-table-container {
height: calc(100vh - 245px);
.content-panel__toolbar {
flex: 0 0 auto;
}
.details-list .mud-table-container {
height: calc(100vh - 200px);
.content-panel__toolbar--scroll {
overflow-x: auto;
white-space: nowrap;
}
.details-tab-contents {
height: calc(100vh - 200px);
.content-panel__body {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
}
.content-panel__container {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.content-panel__table {
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-height: 0;
}
.content-panel__table .mud-table-container {
flex: 1 1 auto;
height: 100%;
}
.content-panel__body > .mud-tabs {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
padding-top: 0;
margin-top: 0;
}
.content-panel__body > .mud-tabs .mud-tabs-tabbar {
margin-bottom: 0;
}
.content-panel__body > .mud-tabs .mud-tabs-panels {
flex: 1 1 auto;
min-height: 0;
display: flex;
flex-direction: column;
overflow: hidden;
padding-top: 0;
margin-top: -1px;
border-top: none;
}
.content-panel__body .mud-tabs .mud-tabs-panels .mud-tab-panel {
overflow: auto;
}
.torrent-list .mud-table-container,
.file-list .mud-table-container,
.details-list .mud-table-container,
.search-list .mud-table-container {
height: calc(100vh - 260px);
height: 100%;
}
.details-tab-contents,
.options-tab-contents,
.rss-contents {
flex: 1 1 auto;
min-height: 0;
overflow: auto;
}
tr.log-normal td {
@@ -220,10 +282,6 @@ td .folder-button {
padding: 6px 16px 6px 16px !important;
}
.rss-contents {
height: calc(100vh - 149px);
}
@keyframes spin {
0% {
transform: rotate(0deg);
@@ -255,4 +313,117 @@ td .folder-button {
.mud-popover .mud-divider:last-child {
display: none;
}
}
:root {
--app-viewport-height: 100vh;
--app-status-bar-height: 35px;
}
@supports (height: 100svh) {
:root {
--app-viewport-height: 100svh;
}
}
@supports ((height: 100dvh) and (not (height: 100svh))) {
:root {
--app-viewport-height: 100dvh;
}
}
html,
body {
height: var(--app-viewport-height);
min-height: var(--app-viewport-height);
}
body {
margin: 0;
overflow: hidden;
overscroll-behavior: none;
}
#app,
.mud-layout {
height: 100%;
min-height: 100%;
}
.app-shell {
display: flex;
flex-direction: column;
height: var(--app-viewport-height);
min-height: var(--app-viewport-height);
overflow: hidden;
}
.app-shell__body {
display: flex;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.app-shell__sidebar {
flex: 0 0 auto;
}
.app-shell__main {
display: flex;
flex-direction: column;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
padding: var(--mud-appbar-height) 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
box-sizing: border-box;
}
.app-shell__status-bar.mud-appbar {
flex: 0 0 calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
height: calc(var(--app-status-bar-height) + env(safe-area-inset-bottom, 0px));
width: 100%;
background-color: var(--mud-palette-dark-lighten);
align-items: center;
justify-content: flex-start;
box-sizing: border-box;
}
.app-shell__status-bar .mud-toolbar {
width: 100%;
height: 100%;
padding-bottom: env(safe-area-inset-bottom, 0px);
background-color: inherit;
box-sizing: border-box;
}
@supports (-webkit-touch-callout: none) {
:root {
--app-viewport-height: -webkit-fill-available;
}
html,
body {
height: -webkit-fill-available;
min-height: -webkit-fill-available;
}
.app-shell {
height: -webkit-fill-available;
min-height: -webkit-fill-available;
}
}
/* Tab bar gap fix */
.content-panel__body > .mud-tabs .mud-tabs-tabbar {
margin-bottom: 0;
padding-bottom: 0;
border-bottom-width: 0;
}
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-wrapper {
margin-bottom: -1px;
}
.content-panel__body > .mud-tabs .mud-tabs-tabbar .mud-tabs-slider {
bottom: 0;
}

View File

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

View File

@@ -5,4 +5,4 @@
// * @author John Doherty <www.johndoherty.info>
// * @license MIT
// */
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e), this.dispatchEvent(new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY })) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);
!function (e, t) { "use strict"; var n = null, a = "PointerEvent" in e || e.navigator && "msPointerEnabled" in e.navigator, i = "ontouchstart" in e || navigator.MaxTouchPoints > 0 || navigator.msMaxTouchPoints > 0, o = a ? "pointerdown" : i ? "touchstart" : "mousedown", r = a ? "pointerup" : i ? "touchend" : "mouseup", m = a ? "pointermove" : i ? "touchmove" : "mousemove", u = a ? "pointerleave" : i ? "touchleave" : "mouseleave", s = 0, c = 0, l = 10, v = 10; function f(e) { p(), e = function (e) { if (void 0 !== e.changedTouches) return e.changedTouches[0]; return e }(e); var n = new CustomEvent("longpress", { bubbles: !0, cancelable: !0, detail: { clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY }, clientX: e.clientX, clientY: e.clientY, offsetX: e.offsetX, offsetY: e.offsetY, pageX: e.pageX, pageY: e.pageY, screenX: e.screenX, screenY: e.screenY }); n.__longPress = !0, this.dispatchEvent(n) || t.addEventListener("click", function e(n) { t.removeEventListener("click", e, !0), function (e) { e.stopImmediatePropagation(), e.preventDefault(), e.stopPropagation() }(n) }, !0) } function d(a) { p(a); var i = a.target, o = parseInt(function (e, n, a) { for (; e && e !== t.documentElement;) { var i = e.getAttribute(n); if (i) return i; e = e.parentNode } return a }(i, "data-long-press-delay", "400"), 10); n = function (t, n) { if (!(e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame && e.mozCancelRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame)) return e.setTimeout(t, n); var a = (new Date).getTime(), i = {}, o = function () { (new Date).getTime() - a >= n ? t.call() : i.value = requestAnimFrame(o) }; return i.value = requestAnimFrame(o), i }(f.bind(i, a), o) } function p(t) { var a; (a = n) && (e.cancelAnimationFrame ? e.cancelAnimationFrame(a.value) : e.webkitCancelAnimationFrame ? e.webkitCancelAnimationFrame(a.value) : e.webkitCancelRequestAnimationFrame ? e.webkitCancelRequestAnimationFrame(a.value) : e.mozCancelRequestAnimationFrame ? e.mozCancelRequestAnimationFrame(a.value) : e.oCancelRequestAnimationFrame ? e.oCancelRequestAnimationFrame(a.value) : e.msCancelRequestAnimationFrame ? e.msCancelRequestAnimationFrame(a.value) : clearTimeout(a)), n = null } "function" != typeof e.CustomEvent && (e.CustomEvent = function (e, n) { n = n || { bubbles: !1, cancelable: !1, detail: void 0 }; var a = t.createEvent("CustomEvent"); return a.initCustomEvent(e, n.bubbles, n.cancelable, n.detail), a }, e.CustomEvent.prototype = e.Event.prototype), e.requestAnimFrame = e.requestAnimationFrame || e.webkitRequestAnimationFrame || e.mozRequestAnimationFrame || e.oRequestAnimationFrame || e.msRequestAnimationFrame || function (t) { e.setTimeout(t, 1e3 / 60) }, t.addEventListener(r, p, !0), t.addEventListener(u, p, !0), t.addEventListener(m, function (e) { var t = Math.abs(s - e.clientX), n = Math.abs(c - e.clientY); (t >= l || n >= v) && p() }, !0), t.addEventListener("wheel", p, !0), t.addEventListener("scroll", p, !0), t.addEventListener(o, function (e) { s = e.clientX, c = e.clientY, d(e) }, !0) }(window, document);

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,44 @@
using Lantean.QBitTorrentClient.Models;
using System.Text.Json;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Converters
{
public sealed class DownloadPathOptionJsonConverter : JsonConverter<DownloadPathOption>
{
public override DownloadPathOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
return reader.TokenType switch
{
JsonTokenType.Null => null,
JsonTokenType.False => new DownloadPathOption(false, null),
JsonTokenType.True => new DownloadPathOption(true, null),
JsonTokenType.String => new DownloadPathOption(true, reader.GetString()),
_ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing download_path.")
};
}
public override void Write(Utf8JsonWriter writer, DownloadPathOption? value, JsonSerializerOptions options)
{
if (value is null)
{
writer.WriteNullValue();
return;
}
if (!value.Enabled)
{
writer.WriteBooleanValue(false);
return;
}
if (string.IsNullOrWhiteSpace(value.Path))
{
writer.WriteBooleanValue(true);
return;
}
writer.WriteStringValue(value.Path);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
using System.Text.Json.Serialization;
using Lantean.QBitTorrentClient.Converters;
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
@@ -7,10 +8,12 @@ namespace Lantean.QBitTorrentClient.Models
[JsonConstructor]
public Category(
string name,
string? savePath)
string? savePath,
DownloadPathOption? downloadPath)
{
Name = name;
SavePath = savePath;
DownloadPath = downloadPath;
}
[JsonPropertyName("name")]
@@ -18,5 +21,9 @@ namespace Lantean.QBitTorrentClient.Models
[JsonPropertyName("savePath")]
public string? SavePath { get; }
[JsonPropertyName("download_path")]
[JsonConverter(typeof(DownloadPathOptionJsonConverter))]
public DownloadPathOption? DownloadPath { get; }
}
}
}

View File

@@ -0,0 +1,15 @@
namespace Lantean.QBitTorrentClient.Models
{
public record DownloadPathOption
{
public DownloadPathOption(bool enabled, string? path)
{
Enabled = enabled;
Path = path;
}
public bool Enabled { get; }
public string? Path { get; }
}
}

View File

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

View File

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

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