Add project files.

This commit is contained in:
ahjephson
2024-04-22 14:15:07 +01:00
parent ce7b627fa9
commit f9847c60f5
166 changed files with 14345 additions and 0 deletions

39
.dcignore Normal file
View File

@@ -0,0 +1,39 @@
# Write glob rules for ignored files.
# Check syntax on https://deepcode.freshdesk.com/support/solutions/articles/60000531055-how-can-i-ignore-files-or-directories-
# Check examples on https://github.com/github/gitignore
# Hidden directories and files
.*
# Common binary directories and files
[Bb]in/
[Oo]bj/
*.exe
*.dll
# Logs and temporary files
[Tt]emp/
*.log
# Build directories
/build/
/dist/
/out/
# Node modules and package directories
node_modules/
**/[Pp]ackages/*
# Various cache directories
*.cache
/saved/
/intermediates/
/generated/
/coverage/
/tmp/
# Specific directory exclusions
/DocProject/Help/html/
# Ignore files from .vs directory
.vs/

View File

@@ -0,0 +1,3 @@
namespace Lantean.QBTFluent.Comparers
{
}

View File

@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.9.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.7.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.7">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,67 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using System.Linq.Expressions;
using System.Text.Json;
using Xunit.Abstractions;
namespace Lantean.QBTBlazor.Test
{
public class UnitTest1
{
private readonly ITestOutputHelper _testOutputHelper;
public UnitTest1(ITestOutputHelper testOutputHelper)
{
_testOutputHelper = testOutputHelper;
}
[Fact]
public void Test()
{
Test2(a => a.Name);
}
private void Test2(Expression<Func<TestClass, object>> expr)
{
var body = expr.Body;
}
[Fact]
public void Create()
{
var propInfo = typeof(TestClass).GetProperty("Name")!;
ParameterExpression expression = Expression.Parameter(typeof(TestClass), "a");
var propertyExpression = Expression.Property(expression, "Value");
var convertExpression = Expression.Convert(propertyExpression, typeof(object));
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
Expression<Func<TestClass, object>> expr2 = a => a.Name;
var x = l.Compile();
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
Assert.Equal(12, res);
expr2.Compile();
}
[Fact]
public void ScanDir()
{
//Dictionary<string, string>
var json = "{\r\n\t\"/this/is/path\": 1,\r\n\t\"/this/other\": 0,\r\n\t\"/home\": \"/path\"\r\n}";
var obj = JsonSerializer.Deserialize<Dictionary<string, SaveLocation>>(json, SerializerOptions.Options);
}
}
public class TestClass
{
public string Name { get; set; }
public string Description { get; set; }
public long Value { get; set; }
}
}

37
Lantean.QBTBlazor.sln Normal file
View File

@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.8.34511.84
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTBlazor.Test", "Lantean.QBTBlazor.Test\Lantean.QBTBlazor.Test.csproj", "{715E075C-1D86-4A7F-BC72-E1E24A294F17}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient", "Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj", "{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMudBlade", "Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{715E075C-1D86-4A7F-BC72-E1E24A294F17}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{715E075C-1D86-4A7F-BC72-E1E24A294F17}.Debug|Any CPU.Build.0 = Debug|Any CPU
{715E075C-1D86-4A7F-BC72-E1E24A294F17}.Release|Any CPU.ActiveCfg = Release|Any CPU
{715E075C-1D86-4A7F-BC72-E1E24A294F17}.Release|Any CPU.Build.0 = Release|Any CPU
{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F0DDAB07-0D6C-40C7-AFE6-08FF2C4CC7E7}.Release|Any CPU.Build.0 = Release|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Debug|Any CPU.Build.0 = Debug|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.ActiveCfg = Release|Any CPU
{83BC76CC-D51B-42AF-A6EE-FA400C300098}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {82E46DB7-956A-4971-BB18-1F20650EC1A4}
EndGlobalSection
EndGlobal

View File

@@ -0,0 +1,12 @@
<Router AppAssembly="@typeof(App).Assembly">
<Found Context="routeData">
<RouteView RouteData="@routeData" DefaultLayout="@typeof(MainLayout)" />
<FocusOnNavigate RouteData="@routeData" Selector="h1" />
</Found>
<NotFound>
<PageTitle>Not found</PageTitle>
<LayoutView Layout="@typeof(MainLayout)">
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>

View File

@@ -0,0 +1,16 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField Label="Category" @bind-Value="Category" />
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save Path" @bind-Value="SavePath" />
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Add</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,31 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddCategoryDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected string? Category { get; set; }
protected string SavePath { get; set; } = "";
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
if (Category is null)
{
return;
}
MudDialog.Close(DialogResult.Ok(new Category(Category, SavePath)));
}
}
}

View File

@@ -0,0 +1,24 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent">
<ButtonTemplate>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Secondary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="@context.Id">
Choose files
</MudButton>
</ButtonTemplate>
</MudFileUpload>
</MudItem>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption />
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Close</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Upload Torrents</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,34 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddTorrentFileDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
protected AddTorrentOptions TorrentOptions { get; set; } = default!;
protected void UploadFiles(IReadOnlyList<IBrowserFile> files)
{
Files = files;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
var options = new AddTorrentFileOptions(Files, TorrentOptions.GetTorrentOptions());
MudDialog.Close(DialogResult.Ok(options));
}
}
}

View File

@@ -0,0 +1,14 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField Label="Urls" Lines="10" @bind-Value="Urls" Variant="Variant.Filled" />
</MudItem>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption />
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Close</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Upload Torrents</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,33 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddTorrentLinkDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected string? Urls { get; set; }
protected AddTorrentOptions TorrentOptions { get; set; } = default!;
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
if (Urls is null)
{
MudDialog.Cancel();
return;
}
var options = new AddTorrentLinkOptions(Urls, TorrentOptions.GetTorrentOptions());
MudDialog.Close(DialogResult.Ok(options));
}
}
}

View File

@@ -0,0 +1,62 @@
<MudItem xs="12">
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Filled">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Filled"></MudTextField>
</MudItem>
@if (ShowCookieOption)
{
<MudItem xs="12">
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Filled"></MudTextField>
</MudItem>
}
<MudItem xs="12">
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Filled"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Filled">
@foreach (var category in Categories)
{
<MudSelectItem Value="category">@category</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudCheckBox Label="Start torrent" @bind-Value="StartTorrent" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudCheckBox Label="Add to top of queue" @bind-Value="AddToTopOfQueue" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Filled">
<MudSelectItem Value="@("None")">None</MudSelectItem>
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudCheckBox Label="Add to top of queue" @bind-Value="AddToTopOfQueue" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudCheckBox Label="Skip hash check" @bind-Value="SkipHashCheck" LabelPosition="LabelPosition.End" />
</MudItem>
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Filled">
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
</MudSelect>
<MudItem xs="12">
<MudCheckBox Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudCheckBox Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Filled" Min="0" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Filled" Min="0" />
</MudItem>

View File

@@ -0,0 +1,79 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class AddTorrentOptions
{
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Parameter]
public bool ShowCookieOption { get; set; }
protected bool TorrentManagementMode { get; set; }
protected string SavePath { get; set; } = default!;
protected string? Cookie { get; set; }
protected string? RenameTorrent { get; set; }
protected IEnumerable<string> Categories { get; set; } = [];
protected string? Category { get; set; }
protected bool StartTorrent { get; set; } = true;
protected bool AddToTopOfQueue { get; set; } = true;
protected string StopCondition { get; set; } = "None";
protected bool SkipHashCheck { get; set; } = false;
protected string ContentLayout { get; set; } = "Original";
protected bool DownloadInSequentialOrder { get; set; } = false;
protected bool DownloadFirstAndLastPiecesFirst { get; set; } = false;
protected long DownloadLimit { get; set; }
protected long UploadLimit { get; set; }
protected override async Task OnInitializedAsync()
{
var categories = await ApiClient.GetAllCategories();
Categories = categories.Select(c => c.Key).ToList();
var preferences = await ApiClient.GetApplicationPreferences();
TorrentManagementMode = preferences.AutoTmmEnabled;
SavePath = preferences.SavePath;
StartTorrent = !preferences.StartPausedEnabled;
AddToTopOfQueue = preferences.AddToTopOfQueue;
StopCondition = preferences.TorrentStopCondition;
ContentLayout = preferences.TorrentContentLayout;
}
public TorrentOptions GetTorrentOptions()
{
return new TorrentOptions(
TorrentManagementMode,
SavePath,
Cookie,
RenameTorrent,
Category,
StartTorrent,
AddToTopOfQueue,
StopCondition,
SkipHashCheck,
ContentLayout,
DownloadInSequentialOrder,
DownloadFirstAndLastPiecesFirst,
DownloadLimit,
UploadLimit);
}
}
}

View File

@@ -0,0 +1,20 @@
@typeparam T
<MudDialog>
<DialogContent>
<MudCard Class="w-100">
<MudList>
@foreach (var column in Columns)
{
<MudListItem>
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumns.Contains(column.Id))" />
</MudListItem>
}
</MudList>
</MudCard>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,51 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class ColumnOptionsDialog<T>
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
[EditorRequired]
public List<ColumnDefinition<T>> Columns { get; set; } = default!;
protected HashSet<string> SelectedColumns { get; set; } = [];
protected override void OnParametersSet()
{
if (SelectedColumns.Count == 0)
{
foreach (var column in Columns.Where(c => c.Enabled))
{
SelectedColumns.Add(column.Id);
}
}
}
protected void SetSelected(bool selected, string id)
{
if (selected)
{
SelectedColumns.Add(id);
}
else
{
SelectedColumns.Remove(id);
}
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(SelectedColumns));
}
}
}

View File

@@ -0,0 +1,9 @@
<MudDialog>
<DialogContent>
@Content
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">@CancelText</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">@SuccessText</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,31 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class ConfirmDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Content { get; set; } = default!;
[Parameter]
public string? SuccessText { get; set; } = "Ok";
[Parameter]
public string? CancelText { get; set; } = "Cancel";
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(true));
}
}
}

View File

@@ -0,0 +1,15 @@
<MudDialog>
<DialogContent>
<MudText>Are you sure you want to remove the selected torrents from the transfer list?</MudText>
<MudGrid>
<MudItem xs="12">
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPosition="LabelPosition.End" />
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Remove</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,24 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class DeleteDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected bool DeleteFiles { get; set; }
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(DeleteFiles));
}
}
}

View File

@@ -0,0 +1,62 @@
@typeparam T
<MudDialog ContentStyle="mix-width: 400px">
<DialogContent>
<MudGrid>
@foreach (var definition in FilterDefinitions ?? [])
{
<MudItem xs="4">
<MudField Label="Column">@definition.Column</MudField>
</MudItem>
<MudItem xs="3">
<MudSelect Label="Operator" T="string" Value="@definition.Operator" ValueChanged="@(v => DefinitionOperatorChanged(definition, v))">
@foreach (var op in Filter.FilterOperator.GetOperatorByDataType(definition.ColumnType))
{
<MudSelectItem T="string" Value="op">@op</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="4">
<MudTextField Label="Value" T="object" Value="@definition.Value" ValueChanged="@(v => DefinitionValueChanged(definition, v))" />
</MudItem>
<MudItem xs="1">
<MudIconButton OnClick="@(e => RemoveDefinition(definition))" Icon="@Icons.Material.Outlined.Remove" />
</MudItem>
<MudDivider />
}
<MudItem xs="4">
<MudSelect Label="Column" T="string" ValueChanged="ColumnChanged">
@foreach (var propertyName in GetAvailablePropertyNames())
{
<MudSelectItem T="string" Value="@propertyName" />
}
</MudSelect>
</MudItem>
<MudItem xs="3">
<MudSelect Label="Operator" T="string" ValueChanged="OperatorChanged">
@if (ColumnType is null)
{
<MudSelectItem T="string" Value="@("")">Please select a column.</MudSelectItem>
}
else
{
foreach (var op in Filter.FilterOperator.GetOperatorByDataType(ColumnType))
{
<MudSelectItem T="string" Value="op">@op</MudSelectItem>
}
}
</MudSelect>
</MudItem>
<MudItem xs="4">
<MudTextField Label="Value" T="string" ValueChanged="ValueChanged" />
</MudItem>
<MudItem xs="1">
<MudIconButton OnClick="AddDefinition" Icon="@Icons.Material.Outlined.Add" />
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,127 @@
using Lantean.QBTMudBlade.Filter;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Linq.Expressions;
using System.Reflection;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class FilterOptionsDialog<T>
{
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
protected IReadOnlyList<PropertyInfo> Columns => _properties;
[Parameter]
public List<PropertyFilterDefinition<T>>? FilterDefinitions { get; set; }
protected override void OnParametersSet()
{
// as
}
protected void RemoveDefinition(PropertyFilterDefinition<T> definition)
{
if (FilterDefinitions is null)
{
return;
}
FilterDefinitions.Remove(definition);
}
protected void DefinitionOperatorChanged(PropertyFilterDefinition<T> definition, string @operator)
{
var existingDefinition = FilterDefinitions?.Find(d => d == definition);
if (existingDefinition is null)
{
return;
}
existingDefinition.Operator = @operator;
}
protected void DefinitionValueChanged(PropertyFilterDefinition<T> definition, object? value)
{
var existingDefinition = FilterDefinitions?.Find(d => d == definition);
if (existingDefinition is null)
{
return;
}
existingDefinition.Value = value;
}
protected string? Column { get; set; }
protected Type? ColumnType { get; set; }
protected string? Operator { get; set; }
protected string? Value { get; set; }
protected void ColumnChanged(string column)
{
Column = column;
ColumnType = _properties.FirstOrDefault(p => p.Name == column)?.PropertyType;
}
protected IEnumerable<string> GetAvailablePropertyNames()
{
foreach (var propertyName in _properties.Select(p => p.Name))
{
if (!(FilterDefinitions?.Exists(d => d.Column == propertyName) ?? false))
{
yield return propertyName;
}
}
}
protected void OperatorChanged(string @operator)
{
Operator = @operator;
}
protected void ValueChanged(string value)
{
Value = value;
}
protected async Task AddDefinition()
{
if (Column is null || Operator is null || (FilterDefinitions?.Exists(d => d.Column == Column) ?? false))
{
return;
}
CreateAndAdd(Column, Operator, Value);
await InvokeAsync(StateHasChanged);
}
private void CreateAndAdd(string column, string @operator, object? value)
{
FilterDefinitions ??= [];
FilterDefinitions.Add(new PropertyFilterDefinition<T>(column, @operator, value));
Column = null;
Operator = null;
Value = null;
}
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
if (Column is not null && Operator is not null && !(FilterDefinitions?.Exists(d => d.Column == Column) ?? false))
{
CreateAndAdd(Column, Operator, Value);
}
MudDialog.Close(DialogResult.Ok(FilterDefinitions));
}
}
}

View File

@@ -0,0 +1,15 @@
@typeparam T
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField Label="@Label" Value="@Value" />
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,28 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class SingleFieldDialog<T>
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Label { get; set; } = default!;
[Parameter]
public T? Value { get; set; }
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(Value));
}
}
}

View File

@@ -0,0 +1,18 @@
@typeparam T
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudNumericField Label="@Label" Value="@Value" Min="Min" Max="Max" />
</MudItem>
<MudItem xs="12">
<MudSlider ValueLabel="true" Value="@Value" Min="Min" Max="Max" />
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Cancel</MudButton>
<MudButton Color="Color.Primary" OnClick="Submit">Save</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,34 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class SliderFieldDialog<T>
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string Label { get; set; } = default!;
[Parameter]
public T? Value { get; set; }
[Parameter]
public T? Min { get; set; }
[Parameter]
public T? Max { get; set; }
protected void Cancel(MouseEventArgs args)
{
MudDialog.Cancel();
}
protected void Submit(MouseEventArgs args)
{
MudDialog.Close(DialogResult.Ok(Value));
}
}
}

View File

@@ -0,0 +1,5 @@
<MudDialog>
<DialogContent>
<MudText>Statistics</MudText>
</DialogContent>
</MudDialog>

View File

@@ -0,0 +1,11 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class StatisticsDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
}
}

View File

@@ -0,0 +1,63 @@
@inherits MudTable<T>
@typeparam T
@{
base.BuildRenderTree(__builder);
}
@code {
private RenderFragment ColGroupFragment(IEnumerable<ColumnDefinition<T>> columns) =>
@<NonRendering>
@if (MultiSelection)
{
<col />
}
@foreach (var column in columns)
{
var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null;
<col style="@style" />
}
</NonRendering>;
private RenderFragment HeaderContentFragment(IEnumerable<ColumnDefinition<T>> columns) =>
@<NonRendering>
@foreach (var column in columns)
{
<MudTh>
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="T" SortDirectionChanged="@(c => SetSort(column.SortSelector, c))">@column.Header</MudTableSortLabel>
}
else
{
@column.Header
}
</MudTh>
}
</NonRendering>;
private RenderFragment<T> RowTemplateFragment(IEnumerable<ColumnDefinition<T>> columns) => data =>
@<NonRendering>
@foreach (var column in columns)
{
<MudTd DataLabel="@column.Header" Class="@column.Class">
@column.RowTemplate(column.GetRowContext(data))
</MudTd>
}
</NonRendering>;
private RenderFragment<T> RowTemplateFragment2(IEnumerable<ColumnDefinition<T>> columns)
{
return context => __builder =>
{
foreach (var column in columns)
{
<MudTd DataLabel="@column.Header" Class="@column.Class">
@column.RowTemplate(column.GetRowContext(context))
</MudTd>
}
};
}
}

View File

@@ -0,0 +1,153 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
public partial class ExtendedTable<T> : MudTable<T>
{
[Parameter]
public IEnumerable<ColumnDefinition<T>>? ColumnDefinitions { get; set; }
[Parameter]
public HashSet<ColumnDefinition<T>> SelectedColumns { get; set; } = [];
private Func<T, object?>? _sortSelector;
private SortDirection _sortDirection;
private IEnumerable<string>? _selectedColumns;
protected override void OnParametersSet()
{
if (ColumnDefinitions is not null)
{
var activeColumns = GetActiveColummns(ColumnDefinitions);
ColGroup ??= ColGroupFragment(activeColumns);
HeaderContent ??= HeaderContentFragment(activeColumns);
RowTemplate ??= RowTemplateFragment(activeColumns);
_selectedColumns ??= ColumnDefinitions.Where(c => c.Enabled).Select(c => c.Id).ToList();
_sortSelector ??= ColumnDefinitions.First(c => c.Enabled).SortSelector;
Items = GetOrderedItems(Items, _sortSelector);
}
base.OnParametersSet();
}
private IEnumerable<T>? GetOrderedItems(IEnumerable<T>? items, Func<T, object?> sortSelector)
{
if (items is null)
{
return null;
}
return items.OrderByDirection(_sortDirection, sortSelector);
}
private void SetSort(Func<T, object?> sortSelector, SortDirection sortDirection)
{
_sortSelector = sortSelector;
_sortDirection = sortDirection;
}
private IEnumerable<ColumnDefinition<T>>? GetColumns()
{
if (ColumnDefinitions is null)
{
return null;
}
return GetActiveColummns(ColumnDefinitions);
}
private IEnumerable<ColumnDefinition<T>> GetActiveColummns(IEnumerable<ColumnDefinition<T>> columns)
{
if (_selectedColumns is null)
{
return columns;
}
return columns.Where(c => _selectedColumns.Contains(c.Id));
}
//private RenderFragment CreateColGroup()
//{
// return builder =>
// {
// var selectedColumns = GetColumns();
// if (selectedColumns is null)
// {
// return;
// }
// if (MultiSelection)
// {
// builder.OpenElement(0, "col");
// builder.CloseElement();
// }
// int sequence = 1;
// foreach (var width in selectedColumns.Select(c => c.Width))
// {
// builder.OpenElement(sequence++, "col");
// if (width.HasValue)
// {
// builder.AddAttribute(sequence++, "style", $"width: {width.Value}px");
// }
// builder.CloseElement();
// }
// };
//}
//private RenderFragment CreateHeaderContent()
//{
// return builder =>
// {
// var selectedColumns = GetColumns();
// if (selectedColumns is null)
// {
// return;
// }
// int sequence = 0;
// foreach (var columnDefinition in selectedColumns)
// {
// builder.OpenComponent<MudTh>(sequence);
// if (columnDefinition.SortSelector is not null)
// {
// builder.OpenComponent<MudTableSortLabel<T>>(sequence++);
// builder.AddAttribute(sequence++, "SortDirectionChanged", EventCallback.Factory.Create<SortDirection>(this, c => SetSort(columnDefinition.SortSelector, c)));
// RenderFragment childContent = b => b.AddContent(0, columnDefinition.Header);
// builder.AddAttribute(sequence++, "ChildContent", childContent);
// builder.CloseComponent();
// }
// else
// {
// RenderFragment childContent = b => b.AddContent(0, columnDefinition.Header);
// builder.AddAttribute(sequence++, "ChildContent", childContent);
// }
// builder.CloseComponent();
// }
// };
//}
//private RenderFragment<T> CreateRowTemplate()
//{
// return context => builder =>
// {
// var selectedColumns = GetColumns();
// if (selectedColumns is null)
// {
// return;
// }
// int sequence = 0;
// foreach (var columnDefinition in selectedColumns)
// {
// builder.OpenComponent<MudTd>(sequence++);
// builder.AddAttribute(sequence++, "DataLabel", columnDefinition.Header);
// builder.AddAttribute(sequence++, "Class", columnDefinition.Class);
// RenderFragment childContent = b => b.AddContent(0, columnDefinition.RowTemplate(columnDefinition.GetRowContext(context)));
// builder.AddAttribute(sequence++, "ChildContent", childContent);
// builder.CloseComponent();
// }
// };
//}
}
}

View File

@@ -0,0 +1,10 @@
<div @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)"
class="@LinkClassname">
@if (!string.IsNullOrEmpty(Icon))
{
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
}
<div Class="mud-nav-link-text">
@ChildContent
</div>
</div>

View File

@@ -0,0 +1,72 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Routing;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using MudBlazor.Utilities;
namespace Lantean.QBTMudBlade.Components
{
public partial class FakeNavLink
{
[Parameter]
public bool Active { get; set; }
[Parameter]
public bool Disabled { get; set; }
[Parameter]
public string? Class { get; set; }
[Parameter]
public bool DisableRipple { get; set; }
/// <summary>
/// Icon to use if set.
/// </summary>
[Parameter]
public string? Icon { get; set; }
/// <summary>
/// The color of the icon. It supports the theme colors, default value uses the themes drawer icon color.
/// </summary>
[Parameter]
public Color IconColor { get; set; } = Color.Default;
[Parameter]
public string? Target { get; set; }
[Parameter]
public EventCallback<MouseEventArgs> OnClick { get; set; }
[Parameter]
public RenderFragment? ChildContent { get; set; }
protected string Classname =>
new CssBuilder("mud-nav-item")
.AddClass($"mud-ripple", !DisableRipple && !Disabled)
.AddClass(Class)
.Build();
protected string LinkClassname =>
new CssBuilder("mud-nav-link")
.AddClass($"mud-nav-link-disabled", Disabled)
.AddClass("active", Active)
.Build();
protected string IconClassname =>
new CssBuilder("mud-nav-link-icon")
.AddClass($"mud-nav-link-icon-default", IconColor == Color.Default)
.Build();
protected async Task OnClickHandler(MouseEventArgs ev)
{
if (Disabled)
{
return;
}
await OnClick.InvokeAsync(ev);
}
}
}

View File

@@ -0,0 +1,99 @@
<MudTable T="ContentItem" Hover="true" FixedHeader="true" HeaderClass="table-head-bordered" Breakpoint="Breakpoint.None" Bordered="false"
MultiSelection="true" Dense="true" SelectOnRowClick="false"
Items="Files"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChanged"
OnRowClick="RowClick"
RowStyleFunc="RowStyle"
RowClassFunc="RowClass"
AllowUnsorted="false">
<ToolBarContent>
<MudToolBar DisableGutters="true" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFile" 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">
<MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability" OnTouch="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability" OnTouch="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
<MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability" OnTouch="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability" OnTouch="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles" OnTouch="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" />
</MudToolBar>
<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>
</ToolBarContent>
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())
{
var style = column.Width.HasValue ? $"width: {column.Width.Value}px" : null;
<col style="@style" />
}
</ColGroup>
<HeaderContent>
@foreach (var column in GetColumns())
{
<MudTh>
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="Torrent" SortDirectionChanged="@(c => SetSort(column.SortSelector, c))" InitialDirection="column.InitialDirection">@column.Header</MudTableSortLabel>
}
else
{
@column.Header
}
</MudTh>
}
</HeaderContent>
<RowTemplate>
@foreach (var column in GetColumns())
{
<MudTd DataLabel="@column.Header" Class="@column.Class">
@column.RowTemplate(column.GetRowContext(context))
</MudTd>
}
</RowTemplate>
</MudTable>
@code {
private RenderFragment<RowContext<ContentItem>> NameColumn
{
get
{
return context => __builder =>
{
<div style="@($"margin-left: {context.Data.Level * 56}px")">
@if (context.Data.IsFolder)
{
<MudIconButton ButtonType="ButtonType.Button" Icon="@Icons.Material.Filled.ExpandLess" Class="@("pa-0 " + (ExpandedNodes.Contains(context.Data.Name) ? "rotate-180" : "rotate-90"))" OnClick="@(c => ToggleNode(context.Data, c))"></MudIconButton>
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-2" Style="margin-right: 2px" />
}
@context.Data.DisplayName
</div>;
};
}
}
private RenderFragment<RowContext<ContentItem>> PriorityColumn
{
get
{
return context => __builder =>
{
<MudSelect T="Priority" Dense="true" Value="@context.Data.Priority" ValueChanged="@(priority => PriorityValueChanged(context.Data, priority))" Class="mt-0">
<MudSelectItem T="Priority" Value="Priority.DoNotDownload">Do not download</MudSelectItem>
<MudSelectItem T="Priority" Value="Priority.Normal">Normal</MudSelectItem>
<MudSelectItem T="Priority" Value="Priority.High">High</MudSelectItem>
<MudSelectItem T="Priority" Value="Priority.Maximum">Maximum</MudSelectItem>
</MudSelect>
};
}
}
}

View File

@@ -0,0 +1,561 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Filter;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Collections.ObjectModel;
using System.Net;
using static MudBlazor.CategoryTypes;
namespace Lantean.QBTMudBlade.Components
{
public partial class FilesTab : IAsyncDisposable
{
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
private Func<ContentItem, object?> SortSelector { get; set; } = c => c.Name;
private SortDirection SortDirection { get; set; } = SortDirection.Ascending;
[Parameter]
public bool Active { get; set; }
[Parameter, EditorRequired]
public string? Hash { get; set; }
[CascadingParameter]
public int RefreshInterval { get; set; }
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
protected HashSet<string> ExpandedNodes { get; set; } = [];
protected Dictionary<string, ContentItem>? FileList { get; set; }
protected IEnumerable<ContentItem> Files => GetFiles();
protected HashSet<ContentItem> SelectedItems { get; set; } = [];
protected List<ColumnDefinition<ContentItem>> _columns = [];
protected ContentItem? SelectedItem { get; set; }
protected string? SearchText { get; set; }
protected int? _selectedIndex { get; set; }
protected HashSet<string> SelectedColumns { get; set; }
public IEnumerable<Func<ContentItem, bool>>? Filters { get; set; }
public FilesTab()
{
_columns.Add(CreateColumnDefinition("Name", c => c.Name, NameColumn, width: 200, initialDirection: SortDirection.Ascending));
_columns.Add(CreateColumnDefinition("Total Size", c => c.Size, c => DisplayHelpers.Size(c.Size)));
_columns.Add(CreateColumnDefinition("Progress", c => c.Progress, c => DisplayHelpers.Percentage(c.Progress)));
_columns.Add(CreateColumnDefinition("Priority", c => c.Priority, PriorityColumn));
_columns.Add(CreateColumnDefinition("Remaining", c => c.Remaining, c => DisplayHelpers.Size(c.Remaining)));
_columns.Add(CreateColumnDefinition("Availability", c => c.Availability, c => c.Availability.ToString("0.00")));
SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet();
}
protected IEnumerable<ColumnDefinition<ContentItem>> GetColumns()
{
return _columns.Where(c => SelectedColumns.Contains(c.Id));
}
protected async Task ColumnOptions()
{
DialogParameters parameters = new DialogParameters
{
{ "Columns", _columns }
};
var reference = await DialogService.ShowAsync<ColumnOptionsDialog<ContentItem>>("ColumnOptions", parameters, DialogHelper.FormDialogOptions);
var result = await reference.Result;
if (result.Canceled)
{
return;
}
SelectedColumns = (HashSet<string>)result.Data;
}
protected async Task ShowFilterDialog()
{
var parameters = new DialogParameters
{
{ nameof(FilterOptionsDialog<ContentItem>.FilterDefinitions), Filters },
};
var result = await DialogService.ShowAsync<FilterOptionsDialog<ContentItem>>("Filters", parameters, DialogHelper.FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
var filterDefinitions = (List<PropertyFilterDefinition<ContentItem>>?)dialogResult.Data;
if (filterDefinitions is null)
{
return;
}
var filters = new List<Func<ContentItem, bool>>();
foreach (var filterDefinition in filterDefinitions)
{
var expression = Filter.FilterExpressionGenerator.GenerateExpression(filterDefinition, false);
filters.Add(expression.Compile());
}
Filters = filters;
}
protected async Task RemoveFilter()
{
Filters = null;
await InvokeAsync(StateHasChanged);
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
protected static float CalculateProgress(IEnumerable<ContentItem> items)
{
return (float)items.Sum(i => i.Downloaded) / items.Sum(i => i.Size);
}
protected static Priority GetPriority(IEnumerable<ContentItem> items)
{
var distinctPriorities = items.Select(i => i.Priority).Distinct();
if (distinctPriorities.Count() == 1)
{
return distinctPriorities.First();
}
return Priority.Mixed;
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing && Files is not null)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.Delay(0);
}
_disposedValue = true;
}
}
protected async Task SearchTextChanged(string value)
{
SearchText = value;
await InvokeAsync(StateHasChanged);
if (FileList is null)
{
return;
}
SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
protected async Task EnabledValueChanged(ContentItem contentItem, bool value)
{
if (Hash is null)
{
return;
}
await ApiClient.SetFilePriority(Hash, [contentItem.Index], MapPriority(value ? Priority.Normal : Priority.DoNotDownload));
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender)
{
return;
}
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (Active && Hash is not null)
{
IReadOnlyList<QBitTorrentClient.Models.FileData> files;
try
{
files = await ApiClient.GetTorrentContents(Hash);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
if (FileList is null)
{
FileList = DataManager.CreateContentsList(files);
}
else
{
DataManager.MergeContentsList(files, FileList);
}
}
await InvokeAsync(StateHasChanged);
}
}
}
protected override async Task OnParametersSetAsync()
{
if (Hash is null)
{
return;
}
if (!Active)
{
return;
}
var contents = await ApiClient.GetTorrentContents(Hash);
FileList = DataManager.CreateContentsList(contents);
SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
{
if (Hash is null)
{
return;
}
IEnumerable<int> fileIndexes;
if (contentItem.IsFolder)
{
fileIndexes = GetChildren(contentItem).Where(c => !c.IsFolder).Select(c => c.Index);
}
else
{
fileIndexes = [contentItem.Index];
}
await ApiClient.SetFilePriority(Hash, fileIndexes, MapPriority(priority));
}
protected string RowClass(ContentItem contentItem, int index)
{
if (contentItem.Level == 0)
{
return "d-table-row";
}
if (ExpandedNodes.Contains(contentItem.Path))
{
return "d-table-row";
}
return "d-none";
}
protected async Task RenameFile()
{
if (Hash is null || FileList is null || _selectedIndex is null)
{
return;
}
var contentItem = FileList.Values.FirstOrDefault(c => c.Index == _selectedIndex.Value);
if (contentItem is null)
{
return;
}
var name = contentItem.GetFileName();
await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async v => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + v));
}
protected void RowClick(TableRowClickEventArgs<ContentItem> eventArgs)
{
_selectedIndex = eventArgs.Item.Index;
}
protected string RowStyle(ContentItem item, int index)
{
var style = "user-select: none; cursor: pointer;";
if (_selectedIndex == item.Index)
{
style += " background: #D3D3D3";
}
return style;
}
protected async Task SelectedItemsChanged(HashSet<ContentItem> selectedItems)
{
if (Hash is null || Files is null)
{
return;
}
var unselectedItems = Files.Except(SelectedItems);
if (unselectedItems.Any())
{
await ApiClient.SetFilePriority(Hash, unselectedItems.Select(c => c.Index), QBitTorrentClient.Models.Priority.DoNotDownload);
foreach (var item in unselectedItems)
{
Files.First(f => f == item).Priority = Priority.DoNotDownload;
}
await InvokeAsync(StateHasChanged);
}
var existingDoNotDownloads = Files.Where(f => f.Priority == Priority.DoNotDownload);
var newlySelectedFiles = selectedItems.Where(f => existingDoNotDownloads.Contains(f));
if (newlySelectedFiles.Any())
{
await ApiClient.SetFilePriority(Hash, newlySelectedFiles.Select(c => c.Index), QBitTorrentClient.Models.Priority.Normal);
foreach (var item in newlySelectedFiles)
{
Files.First(f => f == item).Priority = Priority.Normal;
}
await InvokeAsync(StateHasChanged);
}
}
private void SetSort(Func<ContentItem, object?> sortSelector, SortDirection sortDirection)
{
SortSelector = sortSelector;
SortDirection = sortDirection;
}
protected void ToggleNode(ContentItem contentItem, MouseEventArgs args)
{
if (ExpandedNodes.Contains(contentItem.Name))
{
ExpandedNodes.Remove(contentItem.Name);
}
else
{
ExpandedNodes.Add(contentItem.Name);
}
}
private static QBitTorrentClient.Models.Priority MapPriority(Priority priority)
{
return (QBitTorrentClient.Models.Priority)(int)priority;
}
private IEnumerable<ContentItem> GetChildren(ContentItem contentItem)
{
if (!contentItem.IsFolder || Files is null)
{
return [];
}
return Files.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
}
private IEnumerable<ContentItem> GetDescendants(ContentItem folder, int level)
{
level++;
var descendantsKey = folder.GetDescendantsKey(level);
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey)).OrderByDirection(SortDirection, SortSelector))
{
if (item.IsFolder)
{
var descendants = GetDescendants(item, level);
// if the filter returns some resutls then show folder item
if (descendants.Any())
{
yield return item;
}
// 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)
{
foreach (var filter in Filters)
{
var result = filter(item);
if (!result)
{
return false;
}
}
}
if (!FilterHelper.FilterTerms(item.Name, SearchText))
{
return false;
}
return true;
}
private ReadOnlyCollection<ContentItem> GetFiles()
{
if (FileList is null)
{
return new ReadOnlyCollection<ContentItem>([]);
}
var maxLevel = FileList.Values.Max(f => f.Level);
// this is a flat file structure
if (maxLevel == 0)
{
return FileList.Values.Where(FilterContentItem).OrderByDirection(SortDirection, SortSelector).ToList().AsReadOnly();
}
var list = new List<ContentItem>();
var folders = FileList.Values.Where(c => c.IsFolder && c.Level == 0).OrderByDirection(SortDirection, SortSelector).ToList();
foreach (var folder in folders)
{
list.Add(folder);
var level = 0;
var descendants = GetDescendants(folder, level);
foreach (var descendant in descendants)
{
list.Add(descendant);
}
}
return list.AsReadOnly();
}
protected async Task DoNotDownloadLessThan100PercentAvailability()
{
await LessThanXAvailability(1f, QBitTorrentClient.Models.Priority.DoNotDownload);
}
protected async Task DoNotDownloadLessThan80PercentAvailability()
{
await LessThanXAvailability(0.8f, QBitTorrentClient.Models.Priority.DoNotDownload);
}
protected async Task DoNotDownloadCurrentlyFilteredFiles()
{
await CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority.DoNotDownload);
}
protected async Task NormalPriorityLessThan100PercentAvailability()
{
await LessThanXAvailability(1f, QBitTorrentClient.Models.Priority.Normal);
}
protected async Task NormalPriorityLessThan80PercentAvailability()
{
await LessThanXAvailability(0.8f, QBitTorrentClient.Models.Priority.Normal);
}
protected async Task NormalPriorityCurrentlyFilteredFiles()
{
await CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority.Normal);
}
private async Task LessThanXAvailability(float value, QBitTorrentClient.Models.Priority priority)
{
if (Hash is null || FileList is null)
{
return;
}
var files = FileList.Values.Where(f => f.Availability < value).Select(f => f.Index);
if (!files.Any())
{
return;
}
await ApiClient.SetFilePriority(Hash, files, priority);
}
protected async Task CurrentlyFilteredFiles(QBitTorrentClient.Models.Priority priority)
{
if (Hash is null || FileList is null)
{
return;
}
var files = GetFiles().Select(f => f.Index);
if (!files.Any())
{
return;
}
await ApiClient.SetFilePriority(Hash, files, priority);
}
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, RenderFragment<RowContext<ContentItem>> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
{
var cd = new ColumnDefinition<ContentItem>(name, selector, rowTemplate);
cd.Class = "no-wrap";
if (tdClass is not null)
{
cd.Class += " " + tdClass;
}
cd.Width = width;
cd.Enabled = enabled;
cd.InitialDirection = initialDirection;
return cd;
}
private static ColumnDefinition<ContentItem> CreateColumnDefinition(string name, Func<ContentItem, object?> selector, Func<ContentItem, string>? formatter = null, int? width = null, string? tdClass = null, bool enabled = true, SortDirection initialDirection = SortDirection.None)
{
var cd = new ColumnDefinition<ContentItem>(name, selector, formatter);
cd.Class = "no-wrap";
if (tdClass is not null)
{
cd.Class += " " + tdClass;
}
cd.Width = width;
cd.Enabled = enabled;
cd.InitialDirection = initialDirection;
return cd;
}
}
}

View File

@@ -0,0 +1,27 @@
<MudNavMenu Dense="true">
<MudNavGroup Title="Status" @bind-Expanded="_statusExpanded">
@foreach (var (status, count) in Statuses)
{
var (icon, color) = DisplayHelpers.GetStatusIcon(status);
<FakeNavLink Active="@(Status == status)" Icon="@icon" IconColor="@color" OnClick="@(v => StatusValueChanged(status))">@($"{status.GetStatusName()} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Categories" @bind-Expanded="_categoriesExpanded">
@foreach (var (category, count) in Categories)
{
<FakeNavLink Active="@(Category == category)" Icon="@Icons.Material.Filled.List" IconColor="Color.Info" OnClick="@(v => CategoryValueChanged(category))">@($"{category} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Tags" @bind-Expanded="_tagsExpanded">
@foreach (var (tag, count) in Tags)
{
<FakeNavLink Active="@(Tag == tag)" Icon="@Icons.Material.Filled.Label" IconColor="Color.Info" OnClick="@(v => TagValueChanged(tag))">@($"{tag} ({count})")</FakeNavLink>
}
</MudNavGroup>
<MudNavGroup Title="Trackers" @bind-Expanded="_trackersExpanded">
@foreach (var (tracker, count) in Trackers)
{
<FakeNavLink Active="@(Tracker == tracker)" Icon="@Icons.Material.Filled.PinDrop" IconColor="Color.Info" OnClick="@(v => TrackerValueChanged(tracker))">@($"{GetHostName(tracker)} ({count})")</FakeNavLink>
}
</MudNavGroup>
</MudNavMenu>

View File

@@ -0,0 +1,84 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
public partial class FiltersNav
{
private bool _statusExpanded = true;
private bool _categoriesExpanded = true;
private bool _tagsExpanded = true;
private bool _trackersExpanded = true;
protected string Status { get; set; } = Models.Status.All.ToString();
protected string Category { get; set; } = FilterHelper.CATEGORY_ALL;
protected string Tag { get; set; } = FilterHelper.TAG_ALL;
protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL;
[CascadingParameter]
public MainData? MainData { get; set; }
[Parameter]
public EventCallback<string> CategoryChanged { get; set; }
[Parameter]
public EventCallback<Status> StatusChanged { get; set; }
[Parameter]
public EventCallback<string> TagChanged { get; set; }
[Parameter]
public EventCallback<string> TrackerChanged { get; set; }
public Dictionary<string, int> Tags => MainData?.TagState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
public Dictionary<string, int> Categories => MainData?.CategoriesState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
public Dictionary<string, int> Trackers => MainData?.TrackersState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
public Dictionary<string, int> Statuses => MainData?.StatusState.ToDictionary(d => d.Key, d => d.Value.Count) ?? [];
protected async Task StatusValueChanged(string value)
{
Status = value;
await StatusChanged.InvokeAsync(Enum.Parse<Status>(value));
}
protected async Task CategoryValueChanged(string value)
{
Category = value;
await CategoryChanged.InvokeAsync(value);
}
protected async Task TagValueChanged(string value)
{
Tag = value;
await TagChanged.InvokeAsync(value);
}
protected async Task TrackerValueChanged(string value)
{
Tracker = value;
await TrackerChanged.InvokeAsync(value);
}
protected static string GetHostName(string tracker)
{
try
{
var uri = new Uri(tracker);
return uri.Host;
}
catch
{
return tracker;
}
}
}
}

View File

@@ -0,0 +1,97 @@
<div class="pl-6 pt-6"><MudText Typo="Typo.h6">Transfer</MudText></div>
<MudGrid Class="pl-6 pr-6 pb-6">
<MudItem xs="4">
<MudField Label="Time Active">@DisplayHelpers.Duration(Properties?.TimeElapsed)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="ETA">@DisplayHelpers.Duration(Properties?.EstimatedTimeOfArrival)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Connections">@DisplayHelpers.Duration(Properties?.Connections) @DisplayHelpers.EmptyIfNull(Properties?.ConnectionsLimit, "(", " max)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Downloaded">@DisplayHelpers.Size(Properties?.TotalDownloaded) @DisplayHelpers.Size(Properties?.TotalDownloadedSession, "(", " this session)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Uploaded">@DisplayHelpers.Size(Properties?.TotalUploaded) @DisplayHelpers.Size(Properties?.TotalUploaded, "(", " this session)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Seeds">@DisplayHelpers.Size(Properties?.Seeds) @DisplayHelpers.EmptyIfNull(Properties?.Seeds, "(", " total)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Download Speed">@DisplayHelpers.Speed(Properties?.DownloadSpeed) @DisplayHelpers.Speed(Properties?.DownloadSpeedAverage, "(", " avg.)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Upload Speed">@DisplayHelpers.Speed(Properties?.UploadSpeed) @DisplayHelpers.Speed(Properties?.UploadSpeedAverage, "(", " avg.)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Peers">@DisplayHelpers.Size(Properties?.Peers) @DisplayHelpers.EmptyIfNull(Properties?.Peers, "(", " total)")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Download Limit">@DisplayHelpers.Speed(Properties?.DownloadLimit)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Upload Limit">@DisplayHelpers.Speed(Properties?.UploadLimit)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Wasted">@DisplayHelpers.Size(Properties?.TotalWasted)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Share Ratio">@Properties?.ShareRatio.ToString("0.00")</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Reannounce In">@DisplayHelpers.Duration(Properties?.Reannounce)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Last Seen Complete">@DisplayHelpers.DateTime(Properties?.LastSeen, "Never")</MudField>
</MudItem>
</MudGrid>
<div class="pl-6 pt-6"><MudText Typo="Typo.h6">Information</MudText></div>
<MudGrid Class="pl-6 pr-6 pb-6">
<MudItem xs="4">
<MudField Label="Total Size">@DisplayHelpers.Size(Properties?.TotalSize)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Pieces">
@if (Properties is not null)
{
<text>@Properties.PiecesNum x @DisplayHelpers.Size(Properties.PieceSize) (have @Properties.PiecesHave)</text>
}
</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Created By">@Properties?.CreatedBy</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Added On">@DisplayHelpers.DateTime(Properties?.AdditionDate)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Completed On">@DisplayHelpers.DateTime(Properties?.CompletionDate)</MudField>
</MudItem>
<MudItem xs="4">
<MudField Label="Created On">@DisplayHelpers.DateTime(Properties?.CreationDate)</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Info Hash v1">@Properties?.InfoHashV1</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Info Hash v2">@Properties?.InfoHashV2</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Save Path">@Properties?.SavePath</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Commenet">@Properties?.Comment</MudField>
</MudItem>
</MudGrid>

View File

@@ -0,0 +1,101 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using System.Net;
namespace Lantean.QBTMudBlade.Components
{
public partial class GeneralTab : IAsyncDisposable
{
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
[Parameter, EditorRequired]
public string? Hash { get; set; }
[Parameter]
public bool Active { get; set; }
[CascadingParameter]
public int RefreshInterval { get; set; }
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
protected IReadOnlyList<PieceState> Pieces { get; set; } = [];
protected TorrentProperties Properties { get; set; } = default!;
protected override async Task OnParametersSetAsync()
{
if (Hash is null)
{
return;
}
if (!Active)
{
return;
}
Pieces = await ApiClient.GetTorrentPieceStates(Hash);
Properties = await ApiClient.GetTorrentProperties(Hash);
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (Active && Hash is not null)
{
try
{
Pieces = await ApiClient.GetTorrentPieceStates(Hash);
Properties = await ApiClient.GetTorrentProperties(Hash);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
}
await InvokeAsync(StateHasChanged);
}
}
}
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.Delay(0);
}
_disposedValue = true;
}
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,9 @@
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
<MudMenuItem Icon="@Icons.Material.Filled.PieChart" OnClick="Statistics" OnTouch="Statistics">Statistics</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Settings" OnClick="Settings" OnTouch="Settings">Settings</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Undo" OnClick="ResetWebUI" OnTouch="ResetWebUI">Reset Web UI</MudMenuItem>
<MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Logout" OnClick="Logout" OnTouch="Logout">Logout</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" OnClick="Exit" OnTouch="Exit">Exit qBittorrent</MudMenuItem>
</MudMenu>

View File

@@ -0,0 +1,58 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
public partial class Menu
{
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
protected async Task ResetWebUI()
{
var preferences = new UpdatePreferences
{
AlternativeWebuiPath = null,
AlternativeWebuiEnabled = false,
};
await ApiClient.SetApplicationPreferences(preferences);
NavigationManager.NavigateTo("/", true);
}
protected void Settings()
{
NavigationManager.NavigateTo("/options");
}
protected void Statistics()
{
NavigationManager.NavigateTo("/statistics");
}
protected async Task Logout()
{
await DialogService.ShowConfirmDialog("Logout?", "Are you sure you want to logout?", async () =>
{
await ApiClient.Logout();
NavigationManager.NavigateTo("/login", true);
});
}
protected async Task Exit()
{
await DialogService.ShowConfirmDialog("Quit?", "Are you sure you want to exit qBittorrent?", ApiClient.Shutdown);
}
}
}

View File

@@ -0,0 +1 @@
@ChildContent

View File

@@ -0,0 +1,16 @@
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Components
{
/// <summary>
/// A simple razor wrapper that only renders the child content without any additonal html markup
/// </summary>
public partial class NonRendering
{
/// <summary>
/// The child content to be rendered
/// </summary>
[Parameter]
public RenderFragment? ChildContent { get; set; }
}
}

View File

@@ -0,0 +1,14 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6"></MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,10 @@
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class AdvancedOptions : Options
{
protected override bool SetOptions()
{
return true;
}
}
}

View File

@@ -0,0 +1,69 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Language</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudSelect T="string" Label="User Interface Language" Value="@("en-US")">
<MudSelectItem Value="@("en-US")">English</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Log File</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Log file" Value="FileLogEnabled" ValueChanged="FileLogEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Save Path" Value="FileLogPath" ValueChanged="FileLogPathChanged" Disabled="@(!FileLogEnabled)" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" Label="Backup the log after" Value="FileLogBackupEnabled" ValueChanged="FileLogBackupEnabledChanged" Disabled="@(!FileLogEnabled)" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="9">
<MudNumericField T="int" Label="KiB" Value="FileLogMaxSize" ValueChanged="FileLogMaxSizeChanged" Disabled="@(!FileLogEnabled)" ShrinkLabel Min="1" />
</MudItem>
<MudItem xs="3">
<MudCheckBox T="bool" Label="Delete backups older than" Value="FileLogDeleteOld" ValueChanged="FileLogDeleteOldChanged" Disabled="@(!FileLogEnabled)" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="9">
<MudGrid>
<MudItem xs="9">
<MudNumericField T="int" Value="FileLogAge" ValueChanged="FileLogAgeChanged" Disabled="@(!FileLogEnabled)" ShrinkLabel Min="1" />
</MudItem>
<MudItem xs="3">
<MudSelect T="int" Value="FileLogAgeType" ValueChanged="FileLogAgeTypeChanged" Disabled="@(!FileLogEnabled)" ShrinkLabel>
<MudSelectItem Value="0">days</MudSelectItem>
<MudSelectItem Value="1">months</MudSelectItem>
<MudSelectItem Value="2">years</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Log performance warnings" Value="PerformanceWarning" ValueChanged="PerformanceWarningChanged" LabelPosition="LabelPosition.End" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,99 @@
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class BehaviourOptions : Options
{
protected bool FileLogEnabled { get; set; }
protected string? FileLogPath { get; set; }
protected bool FileLogBackupEnabled { get; set; }
protected int FileLogMaxSize { get; set; }
protected bool FileLogDeleteOld { get; set; }
protected int FileLogAge { get; set; }
protected int FileLogAgeType { get; set; }
protected bool PerformanceWarning { get; set; }
protected override bool SetOptions()
{
if (Preferences is null)
{
return false;
}
FileLogEnabled = Preferences.FileLogEnabled;
FileLogPath = Preferences.FileLogPath;
FileLogBackupEnabled = Preferences.FileLogBackupEnabled;
FileLogMaxSize = Preferences.FileLogMaxSize;
FileLogDeleteOld = Preferences.FileLogDeleteOld;
FileLogAge = Preferences.FileLogAge;
FileLogAgeType = Preferences.FileLogAgeType;
PerformanceWarning = Preferences.PerformanceWarning;
return true;
}
protected async Task FileLogEnabledChanged(bool value)
{
FileLogEnabled = value;
UpdatePreferences.FileLogEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
await InvokeAsync(StateHasChanged);
}
protected async Task FileLogPathChanged(string value)
{
FileLogPath = value;
UpdatePreferences.FileLogPath = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogBackupEnabledChanged(bool value)
{
FileLogBackupEnabled = value;
UpdatePreferences.FileLogBackupEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogMaxSizeChanged(int value)
{
FileLogMaxSize = value;
UpdatePreferences.FileLogMaxSize = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogDeleteOldChanged(bool value)
{
FileLogDeleteOld = value;
UpdatePreferences.FileLogDeleteOld = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogAgeChanged(int value)
{
FileLogAge = value;
UpdatePreferences.FileLogAge = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task FileLogAgeTypeChanged(int value)
{
FileLogAgeType = value;
UpdatePreferences.FileLogAgeType = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task PerformanceWarningChanged(bool value)
{
PerformanceWarning = value;
UpdatePreferences.PerformanceWarning = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
}
}

View File

@@ -0,0 +1,14 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6"></MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,10 @@
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class BitTorrentOptions : Options
{
protected override bool SetOptions()
{
return true;
}
}
}

View File

@@ -0,0 +1,72 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12">
<MudSelect T="int" Label="Peer connection protocol" Value="BittorrentProtocol" ValueChanged="BittorrentProtocolChanged">
<MudSelectItem T="int" Value="0">TCP and μTP</MudSelectItem>
<MudSelectItem T="int" Value="1">TCP</MudSelectItem>
<MudSelectItem T="int" Value="2">μTP</MudSelectItem>
</MudSelect>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Listening Port</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="11">
<MudNumericField T="int" Label="Port used for incoming connections" Value="ListenPort" ValueChanged="ListenPortChanged" Min="@MinPortValue" Max="@MaxPortValue" />
</MudItem>
<MudItem xs="1">
<MudButton OnClick="GenerateRandomPort">Random</MudButton>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Use UPnp / NAT-PMP port forwarding from my router" Value="Upnp" ValueChanged="UpnpChanged" LabelPosition="LabelPosition.End" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Connections Limits</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12" md="6">
<MudCheckBox T="bool" Label="Global maximum number of connections" Value="MaxConnecEnabled" ValueChanged="MaxConnecEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField T="int" Label="Connections" Value="MaxConnec" ValueChanged="MaxConnecChanged" ShrinkLabel Min="1" Disabled="@(!MaxConnecEnabled)" />
</MudItem>
<MudItem xs="12" md="6">
<MudCheckBox T="bool" Label="Maximum number of connections per torrent" Value="MaxConnecPerTorrentEnabled" ValueChanged="MaxConnecPerTorrentEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField T="int" Label="Connections" Value="MaxConnecPerTorrent" ValueChanged="MaxConnecPerTorrentChanged" ShrinkLabel Min="1" Disabled="@(!MaxConnecPerTorrentEnabled)" />
</MudItem>
<MudItem xs="12" md="6">
<MudCheckBox T="bool" Label="Global maximum number of upload slots" Value="MaxUploadsEnabled" ValueChanged="MaxUploadsEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField T="int" Label="Slots" Value="MaxUploads" ValueChanged="MaxUploadsChanged" ShrinkLabel Min="1" Disabled="@(!MaxUploadsEnabled)" />
</MudItem>
<MudItem xs="12" md="6">
<MudCheckBox T="bool" Label="Maximum number of upload slots per torrent" Value="MaxUploadsPerTorrentEnabled" ValueChanged="MaxUploadsPerTorrentEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" md="6">
<MudNumericField T="int" Label="Slots" Value="MaxUploadsPerTorrent" ValueChanged="MaxUploadsPerTorrentChanged" ShrinkLabel Min="1" Disabled="@(!MaxUploadsPerTorrentEnabled)" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,330 @@
using System.Numerics;
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class ConnectionOptions : Options
{
protected int BittorrentProtocol { get; private set; }
protected int ListenPort { get; private set; }
protected bool Upnp { get; private set; }
protected bool MaxConnecEnabled { get; private set; }
protected int MaxConnec { get; private set; }
protected bool MaxConnecPerTorrentEnabled { get; private set; }
protected int MaxConnecPerTorrent { get; private set; }
protected bool MaxUploadsEnabled { get; private set; }
protected int MaxUploads { get; private set; }
protected bool MaxUploadsPerTorrentEnabled { get; private set; }
protected int MaxUploadsPerTorrent { get; private set; }
protected bool I2pEnabled { get; private set; }
protected string? I2pAddress { get; private set; }
protected int I2pPort { get; private set; }
protected bool I2pMixedMode { get; private set; }
protected string? ProxyType { get; private set; }
protected string? ProxyIp { get; private set; }
protected int ProxyPort { get; private set; }
protected bool ProxyAuthEnabled { get; private set; }
protected string? ProxyUsername { get; private set; }
protected string? ProxyPassword { get; private set; }
protected bool ProxyHostnameLookup { get; private set; }
protected bool ProxyBittorrent { get; private set; }
protected bool ProxyPeerConnections { get; private set; }
protected bool ProxyRss { get; private set; }
protected bool ProxyMisc { get; private set; }
protected bool IpFilterEnabled { get; private set; }
protected string? IpFilterPath { get; private set; }
protected bool IpFilterTrackers { get; private set; }
protected string? BannedIPs { get; private set; }
protected override bool SetOptions()
{
if (Preferences is null)
{
return false;
}
BittorrentProtocol = Preferences.BittorrentProtocol;
ListenPort = Preferences.ListenPort;
Upnp = Preferences.Upnp;
if (Preferences.MaxConnec > 0)
{
MaxConnecEnabled = true;
MaxConnec = Preferences.MaxConnec;
}
else
{
MaxConnecEnabled = false;
MaxConnec = 500;
}
if (Preferences.MaxConnecPerTorrent > 0)
{
MaxConnecPerTorrentEnabled = true;
MaxConnecPerTorrent = Preferences.MaxConnecPerTorrent;
}
else
{
MaxConnecPerTorrentEnabled = false;
MaxConnecPerTorrent = 100;
}
if (Preferences.MaxUploads > 0)
{
MaxUploadsEnabled = true;
MaxUploads = Preferences.MaxUploads;
}
else
{
MaxUploadsEnabled = false;
MaxUploads = 20;
}
if (Preferences.MaxUploadsPerTorrent > 0)
{
MaxUploadsPerTorrentEnabled = true;
MaxUploadsPerTorrent = Preferences.MaxUploadsPerTorrent;
}
else
{
MaxUploadsPerTorrentEnabled = false;
MaxUploadsPerTorrent = 4;
}
I2pEnabled = Preferences.I2pEnabled;
I2pAddress = Preferences.I2pAddress;
I2pPort = Preferences.I2pPort;
I2pMixedMode = Preferences.I2pMixedMode;
ProxyType = Preferences.ProxyType;
ProxyIp = Preferences.ProxyIp;
ProxyPort = Preferences.ProxyPort;
ProxyAuthEnabled = Preferences.ProxyAuthEnabled;
ProxyUsername = Preferences.ProxyUsername;
ProxyPassword = Preferences.ProxyPassword;
ProxyHostnameLookup = Preferences.ProxyHostnameLookup;
ProxyBittorrent = Preferences.ProxyBittorrent;
ProxyPeerConnections = Preferences.ProxyPeerConnections;
ProxyRss = Preferences.ProxyRss;
ProxyMisc = Preferences.ProxyMisc;
IpFilterEnabled = Preferences.IpFilterEnabled;
IpFilterPath = Preferences.IpFilterPath;
IpFilterTrackers = Preferences.IpFilterTrackers;
BannedIPs = Preferences.BannedIPs;
return true;
}
protected async Task BittorrentProtocolChanged(int value)
{
BittorrentProtocol = value;
UpdatePreferences.BittorrentProtocol = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ListenPortChanged(int value)
{
ListenPort = value;
UpdatePreferences.ListenPort = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task UpnpChanged(bool value)
{
Upnp = value;
UpdatePreferences.Upnp = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void MaxConnecEnabledChanged(bool value)
{
MaxConnecEnabled = value;
}
protected async Task MaxConnecChanged(int value)
{
MaxConnec = value;
UpdatePreferences.MaxConnec = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void MaxConnecPerTorrentEnabledChanged(bool value)
{
MaxConnecPerTorrentEnabled = value;
}
protected async Task MaxConnecPerTorrentChanged(int value)
{
MaxConnecPerTorrent = value;
UpdatePreferences.MaxConnecPerTorrent = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void MaxUploadsEnabledChanged(bool value)
{
MaxUploadsEnabled = value;
}
protected async Task MaxUploadsChanged(int value)
{
MaxUploads = value;
UpdatePreferences.MaxUploads = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void MaxUploadsPerTorrentEnabledChanged(bool value)
{
MaxUploadsPerTorrentEnabled = value;
}
protected async Task MaxUploadsPerTorrentChanged(int value)
{
MaxUploadsPerTorrent = value;
UpdatePreferences.MaxUploadsPerTorrent = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task I2pEnabledChanged(bool value)
{
I2pEnabled = value;
UpdatePreferences.I2pEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task I2pAddressChanged(string value)
{
I2pAddress = value;
UpdatePreferences.I2pAddress = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task I2pPortChanged(int value)
{
I2pPort = value;
UpdatePreferences.I2pPort = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task I2pMixedModeChanged(bool value)
{
I2pMixedMode = value;
UpdatePreferences.I2pMixedMode = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyTypeChanged(string value)
{
ProxyType = value;
UpdatePreferences.ProxyType = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyIpChanged(string value)
{
ProxyIp = value;
UpdatePreferences.ProxyIp = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyPortChanged(int value)
{
ProxyPort = value;
UpdatePreferences.ProxyPort = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyAuthEnabledChanged(bool value)
{
ProxyAuthEnabled = value;
UpdatePreferences.ProxyAuthEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyUsernameChanged(string value)
{
ProxyUsername = value;
UpdatePreferences.ProxyUsername = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyPasswordChanged(string value)
{
ProxyPassword = value;
UpdatePreferences.ProxyPassword = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyHostnameLookupChanged(bool value)
{
ProxyHostnameLookup = value;
UpdatePreferences.ProxyHostnameLookup = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyBittorrentChanged(bool value)
{
ProxyBittorrent = value;
UpdatePreferences.ProxyBittorrent = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyPeerConnectionsChanged(bool value)
{
ProxyPeerConnections = value;
UpdatePreferences.ProxyPeerConnections = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyRssChanged(bool value)
{
ProxyRss = value;
UpdatePreferences.ProxyRss = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ProxyMiscChanged(bool value)
{
ProxyMisc = value;
UpdatePreferences.ProxyMisc = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task IpFilterEnabledChanged(bool value)
{
IpFilterEnabled = value;
UpdatePreferences.IpFilterEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task IpFilterPathChanged(string value)
{
IpFilterPath = value;
UpdatePreferences.IpFilterPath = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task IpFilterTrackersChanged(bool value)
{
IpFilterTrackers = value;
UpdatePreferences.IpFilterTrackers = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task BannedIPsChanged(string value)
{
BannedIPs = value;
UpdatePreferences.BannedIPs = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected const int MinPortValue = 1024;
protected const int MaxPortValue = 65535;
protected async Task GenerateRandomPort()
{
var random = new Random();
var port = random.Next(MinPortValue, MaxPortValue);
await ListenPortChanged(port);
}
}
}

View File

@@ -0,0 +1,287 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">When adding a torrent</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudSelect T="string" Label="Torrent content layout" Value="TorrentContentLayout" ValueChanged="TorrentContentLayoutChanged" ShrinkLabel>
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Add to top of queue" Value="AddToTopOfQueue" ValueChanged="AddToTopOfQueueChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Do not start the download automatically" Value="StartPausedEnabled" ValueChanged="StartPausedEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudSelect T="string" Label="Torrent stop condition" Value="TorrentStopCondition" ValueChanged="TorrentStopConditionChanged" ShrinkLabel>
<MudSelectItem Value="@("None")">None</MudSelectItem>
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
<MudSelectItem Value="@("FilesChecked")">Files Checked</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Delete .torrent files afterwards" Value="AutoDeleteMode" ValueChanged="AutoDeleteModeChanged" LabelPosition="LabelPosition.End" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Pre-allocate disk space for all files" Value="PreallocateAll" ValueChanged="PreallocateAllChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Append .!qB extension to incomplete files" Value="IncompleteFilesExt" ValueChanged="IncompleteFilesExtChanged" LabelPosition="LabelPosition.End" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Saving Management</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" ShrinkLabel>
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="bool" Label="When Torrent Category changed" Value="TorrentChangedTmmEnabled" ValueChanged="TorrentChangedTmmEnabledChanged" ShrinkLabel>
<MudSelectItem Value="true">Relocate torrent</MudSelectItem>
<MudSelectItem Value="false">Switch torrent to Manual Mode</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="bool" Label="When Default Save Path changed" Value="SavePathChangedTmmEnabled" ValueChanged="SavePathChangedTmmEnabledChanged" ShrinkLabel>
<MudSelectItem Value="true">Relocate affected torrents</MudSelectItem>
<MudSelectItem Value="false">Switch affected torrents to Manual Mode</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudSelect T="bool" Label="When Category Save Path changed" Value="CategoryChangedTmmEnabled" ValueChanged="CategoryChangedTmmEnabledChanged" ShrinkLabel>
<MudSelectItem Value="true">Relocate affected torrents</MudSelectItem>
<MudSelectItem Value="false">Switch affected torrents to Manual Mode</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Use Subcategories" Value="UseSubcategories" ValueChanged="UseSubcategoriesChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Default Save Path" Value="SavePath" ValueChanged="SavePathChanged" ShrinkLabel />
</MudItem>
<MudItem xs="12">
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCheckBox T="bool" Label="Keep incomplete torrents in" Value="TempPathEnabled" ValueChanged="TempPathEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" sm="6" md="9">
<MudTextField T="string" Label="Path" Value="TempPath" ValueChanged="TempPathChanged" ShrinkLabel Disabled="@(!TempPathEnabled)" />
</MudItem>
</MudGrid>
</MudItem>
<MudItem xs="12">
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCheckBox T="bool" Label="Copy .torrent files to" Value="ExportDirEnabled" ValueChanged="ExportDirEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" sm="6" md="9">
<MudTextField T="string" Label="Path" Value="ExportDir" ValueChanged="ExportDirChanged" ShrinkLabel Disabled="@(!TempPathEnabled)" />
</MudItem>
</MudGrid>
</MudItem>
<MudItem xs="12">
<MudGrid>
<MudItem xs="12" sm="6" md="3">
<MudCheckBox T="bool" Label="Copy .torrent files for finished downloads to" Value="ExportDirFinEnabled" ValueChanged="ExportDirFinEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12" sm="6" md="9">
<MudTextField T="string" Label="Path" Value="ExportDirFin" ValueChanged="ExportDirFinChanged" ShrinkLabel Disabled="@(!TempPathEnabled)" />
</MudItem>
</MudGrid>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Automatically add torrents from</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudSimpleTable>
<thead>
<tr>
<th>Monitored Folder</th>
<th>Override Save Location</th>
</tr>
</thead>
<tbody>
@foreach (var item in ScanDirs)
{
<tr>
<td>
<MudTextField T="string" Label="Path" Value="@item.Key" ValueChanged="@(v => ScanDirsKeyChanged(item.Key, v))" />
</td>
<td>
<MudGrid>
<MudItem xs="@(item.Value.SavePath is null ? 12 : 3)">
<MudSelect T="string" Value="@item.Value.ToString()" ValueChanged="@(v => ScanDirsValueChanged(item.Key, v))">
<MudSelectItem T="string" Value="@("0")">Monitored folder</MudSelectItem>
<MudSelectItem T="string" Value="@("1")">Default save location</MudSelectItem>
<MudSelectItem T="string" Value="@(item.Value.SavePath is not null ? item.Value.SavePath : "")">Other...</MudSelectItem>
</MudSelect>
</MudItem>
@if (item.Value.SavePath is not null)
{
<MudItem xs="9">
<MudTextField T="string" Label="Path" Value="@item.Value.SavePath" ValueChanged="@(v => ScanDirsValueChanged(item.Key, v))" />
</MudItem>
}
</MudGrid>
</td>
</tr>
}
@for (int i = 0; i < AddedScanDirs.Count; i++)
{
var item = AddedScanDirs[i];
var index = i;
var isLast = i == AddedScanDirs.Count - 1;
<tr>
<td>
<MudTextField T="string" Label="Path" Value="@item.Key" ValueChanged="@(v => AddedScanDirsKeyChanged(index, v))" VaMudListItemdation="IsVaMudListItemdNewKey" />
</td>
<td>
<MudGrid>
<MudItem xs="@(item.Value.SavePath is null ? (isLast ? 11 : 12) : 3)">
<MudSelect T="string" Value="@item.Value.ToString()" ValueChanged="@(v => AddedScanDirsValueChanged(index, v))">
<MudSelectItem T="string" Value="@("0")">Monitored folder</MudSelectItem>
<MudSelectItem T="string" Value="@("1")">Default save location</MudSelectItem>
<MudSelectItem T="string" Value="@(item.Value.SavePath is not null ? item.Value.SavePath : "")">Other...</MudSelectItem>
</MudSelect>
</MudItem>
@if (item.Value.SavePath is not null)
{
<MudItem xs="@(isLast ? 8 : 9)">
<MudTextField T="string" Label="Path" Value="@item.Value.SavePath" ValueChanged="@(v => AddedScanDirsValueChanged(index, v))" />
</MudItem>
}
@if (isLast)
{
<MudItem xs="1">
<MudIconButton Icon="@Icons.Material.Outlined.Add" OnCMudListItemck="AddNewScanDir" />
</MudItem>
}
</MudGrid>
</td>
</tr>
}
</tbody>
</MudSimpleTable>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Excluded file names" Value="ExcludedFileNamesEnabled" ValueChanged="ExcludedFileNamesEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Excluded files names" Value="ExcludedFileNames" ValueChanged="ExcludedFileNamesChanged" MudListItemnes="5" ShrinkLabel Disabled="@(!ExcludedFileNamesEnabled)" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent>
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Email notification upon download completion" Value="MailNotificationEnabled" ValueChanged="MailNotificationEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="From" Value="MailNotificationSender" ValueChanged="MailNotificationSenderChanged" ShrinkLabel Disabled="@(!MailNotificationEnabled)" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="To" Value="MailNotificationEmail" ValueChanged="MailNotificationEmailChanged" ShrinkLabel Disabled="@(!MailNotificationEnabled)" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="SMTP server" Value="MailNotificationSmtp" ValueChanged="MailNotificationSmtpChanged" ShrinkLabel Disabled="@(!MailNotificationEnabled)" />
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="This server requires a secure connection (SSL)" Value="MailNotificationSslEnabled" ValueChanged="MailNotificationSslEnabledChanged" LabelPosition="LabelPosition.End" Disabled="@(!MailNotificationEnabled)" />
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Authentication" Value="MailNotificationAuthEnabled" ValueChanged="MailNotificationAuthEnabledChanged" LabelPosition="LabelPosition.End" Disabled="@(!MailNotificationEnabled)" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Username" Value="MailNotificationUsername" ValueChanged="MailNotificationUsernameChanged" ShrinkLabel Disabled="@(!(MailNotificationEnabled && MailNotificationAuthEnabled))" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="Password" Value="MailNotificationPassword" ValueChanged="MailNotificationPasswordChanged" ShrinkLabel Disabled="@(!(MailNotificationEnabled && MailNotificationAuthEnabled))" InputType="InputType.Password" />
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">Run exernal program</MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Run external program on torrent added" Value="AutorunOnTorrentAddedEnabled" ValueChanged="AutorunOnTorrentAddedEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="External program" Value="AutorunOnTorrentAddedProgram" ValueChanged="AutorunOnTorrentAddedProgramChanged" ShrinkLabel Disabled="@(!AutorunOnTorrentAddedEnabled)" />
</MudItem>
<MudItem xs="12">
<MudCheckBox T="bool" Label="Run external program on torrent finished" Value="AutorunEnabled" ValueChanged="AutorunEnabledChanged" LabelPosition="LabelPosition.End" />
</MudItem>
<MudItem xs="12">
<MudTextField T="string" Label="External program" Value="AutorunProgram" ValueChanged="AutorunProgramChanged" ShrinkLabel Disabled="@(!AutorunEnabled)" />
</MudItem>
<MudItem xs="12">
<MudText>Supported parameters (case sensitive):</MudText>
<MudList>
<MudListItem>%N: Torrent name</MudListItem>
<MudListItem>%L: Category</MudListItem>
<MudListItem>%G: Tags (separated by comma)</MudListItem>
<MudListItem>%F: Content path (same as root path for multifile torrent)</MudListItem>
<MudListItem>%R: Root path (first torrent subdirectory path)</MudListItem>
<MudListItem>%D: Save path</MudListItem>
<MudListItem>%C: Number of files</MudListItem>
<MudListItem>%Z: Torrent size (bytes)</MudListItem>
<MudListItem>%T: Current tracker</MudListItem>
<MudListItem>%I: Info hash v1</MudListItem>
<MudListItem>%J: Info hash v2</MudListItem>
<MudListItem>%K: Torrent ID</MudListItem>
</MudList>
</MudItem>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,399 @@
using Lantean.QBitTorrentClient.Models;
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class DownloadsOptions : Options
{
protected string? TorrentContentLayout { get; set; }
protected bool AddToTopOfQueue { get; set; }
protected bool StartPausedEnabled { get; set; }
protected string? TorrentStopCondition { get; set; }
protected bool AutoDeleteMode { get; set; }
protected bool PreallocateAll { get; set; }
protected bool IncompleteFilesExt { get; set; }
protected bool AutoTmmEnabled { get; set; }
protected bool TorrentChangedTmmEnabled { get; set; }
protected bool SavePathChangedTmmEnabled { get; set; }
protected bool CategoryChangedTmmEnabled { get; set; }
protected bool UseSubcategories { get; set; }
protected string? SavePath { get; set; }
protected bool TempPathEnabled { get; set; }
protected string? TempPath { get; set; }
protected bool ExportDirEnabled { get; set; }
protected string? ExportDir { get; set; }
protected bool ExportDirFinEnabled { get; set; }
protected string? ExportDirFin { get; set; }
protected Dictionary<string, SaveLocation> ScanDirs { get; set; } = [];
protected bool ExcludedFileNamesEnabled { get; set; }
protected string? ExcludedFileNames { get; set; }
protected bool MailNotificationEnabled { get; set; }
protected string? MailNotificationSender { get; set; }
protected string? MailNotificationEmail { get; set; }
protected string? MailNotificationSmtp { get; set; }
protected bool MailNotificationSslEnabled { get; set; }
protected bool MailNotificationAuthEnabled { get; set; }
protected string? MailNotificationUsername { get; set; }
protected string? MailNotificationPassword { get; set; }
protected bool AutorunOnTorrentAddedEnabled { get; set; }
protected string? AutorunOnTorrentAddedProgram { get; set; }
protected bool AutorunEnabled { get; set; }
protected string? AutorunProgram { get; set; }
protected List<KeyValuePair<string, SaveLocation>> AddedScanDirs { get; set; } = [];
protected override bool SetOptions()
{
if (Preferences is null)
{
return false;
}
// when adding a torrent
TorrentContentLayout = Preferences.TorrentContentLayout;
AddToTopOfQueue = Preferences.AddToTopOfQueue;
StartPausedEnabled = Preferences.StartPausedEnabled;
TorrentStopCondition = Preferences.TorrentStopCondition;
AutoDeleteMode = Preferences.AutoDeleteMode == 1;
PreallocateAll = Preferences.PreallocateAll;
IncompleteFilesExt = Preferences.IncompleteFilesExt;
// saving management
AutoTmmEnabled = Preferences.AutoTmmEnabled;
TorrentChangedTmmEnabled = Preferences.TorrentChangedTmmEnabled;
SavePathChangedTmmEnabled = Preferences.SavePathChangedTmmEnabled;
CategoryChangedTmmEnabled = Preferences.CategoryChangedTmmEnabled;
UseSubcategories = Preferences.UseSubcategories;
SavePath = Preferences.SavePath;
TempPathEnabled = Preferences.TempPathEnabled;
TempPath = Preferences.TempPath;
ExportDir = Preferences.ExportDir;
ExportDirEnabled = !string.IsNullOrEmpty(Preferences.ExportDir);
ExportDirFin = Preferences.ExportDirFin;
ExportDirFinEnabled = !string.IsNullOrEmpty(Preferences.ExportDirFin);
ScanDirs.Clear();
foreach (var dir in Preferences.ScanDirs)
{
ScanDirs.Add(dir.Key, dir.Value);
}
ExcludedFileNamesEnabled = Preferences.ExcludedFileNamesEnabled;
ExcludedFileNames = Preferences.ExcludedFileNames;
// email notification
MailNotificationEnabled = Preferences.MailNotificationEnabled;
MailNotificationSender = Preferences.MailNotificationSender;
MailNotificationEmail = Preferences.MailNotificationEmail;
MailNotificationSmtp = Preferences.MailNotificationSmtp;
MailNotificationSslEnabled = Preferences.MailNotificationSslEnabled;
MailNotificationAuthEnabled = Preferences.MailNotificationAuthEnabled;
MailNotificationUsername = Preferences.MailNotificationUsername;
MailNotificationPassword = Preferences.MailNotificationPassword;
// autorun
AutorunOnTorrentAddedEnabled = Preferences.AutorunOnTorrentAddedEnabled;
AutorunOnTorrentAddedProgram = Preferences.AutorunOnTorrentAddedProgram;
AutorunEnabled = Preferences.AutorunEnabled;
AutorunProgram = Preferences.AutorunProgram;
AddedScanDirs.Clear();
AddDefaultScanDir();
return true;
}
protected async Task TorrentContentLayoutChanged(string value)
{
TorrentContentLayout = value;
UpdatePreferences.TorrentContentLayout = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AddToTopOfQueueChanged(bool value)
{
AddToTopOfQueue = value;
UpdatePreferences.AddToTopOfQueue = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task StartPausedEnabledChanged(bool value)
{
StartPausedEnabled = value;
UpdatePreferences.StartPausedEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task TorrentStopConditionChanged(string value)
{
TorrentStopCondition = value;
UpdatePreferences.TorrentStopCondition = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutoDeleteModeChanged(bool value)
{
AutoDeleteMode = value;
UpdatePreferences.AutoDeleteMode = value ? 1 : 0;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task PreallocateAllChanged(bool value)
{
PreallocateAll = value;
UpdatePreferences.PreallocateAll = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task IncompleteFilesExtChanged(bool value)
{
IncompleteFilesExt = value;
UpdatePreferences.IncompleteFilesExt = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutoTmmEnabledChanged(bool value)
{
AutoTmmEnabled = value;
UpdatePreferences.AutoTmmEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task TorrentChangedTmmEnabledChanged(bool value)
{
TorrentChangedTmmEnabled = value;
UpdatePreferences.TorrentChangedTmmEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task SavePathChangedTmmEnabledChanged(bool value)
{
SavePathChangedTmmEnabled = value;
UpdatePreferences.SavePathChangedTmmEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task CategoryChangedTmmEnabledChanged(bool value)
{
CategoryChangedTmmEnabled = value;
UpdatePreferences.CategoryChangedTmmEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task UseSubcategoriesChanged(bool value)
{
UseSubcategories = value;
UpdatePreferences.UseSubcategories = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task SavePathChanged(string value)
{
SavePath = value;
UpdatePreferences.SavePath = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task TempPathEnabledChanged(bool value)
{
TempPathEnabled = value;
UpdatePreferences.TempPathEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task TempPathChanged(string value)
{
TempPath = value;
UpdatePreferences.TempPath = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void ExportDirEnabledChanged(bool value)
{
ExportDirEnabled = value;
}
protected async Task ExportDirChanged(string value)
{
ExportDir = value;
UpdatePreferences.ExportDir = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected void ExportDirFinEnabledChanged(bool value)
{
ExportDirFinEnabled = value;
}
protected async Task ExportDirFinChanged(string value)
{
ExportDirFin = value;
UpdatePreferences.ExportDirFin = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ScanDirsKeyChanged(string key, string value)
{
if (ScanDirs.Remove(key, out var location))
{
ScanDirs[value] = location;
}
UpdatePreferences.ScanDirs = ScanDirs;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ScanDirsValueChanged(string key, string value)
{
ScanDirs[key] = SaveLocation.Create(value);
UpdatePreferences.ScanDirs = ScanDirs;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AddedScanDirsKeyChanged(int index, string key)
{
if (key == "")
{
return;
}
ScanDirs.Add(key, AddedScanDirs[index].Value);
AddedScanDirs.RemoveAt(index);
UpdatePreferences.ScanDirs = ScanDirs;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
if (AddedScanDirs.Count == 0)
{
AddDefaultScanDir();
}
}
protected void AddedScanDirsValueChanged(int index, string value)
{
var existing = AddedScanDirs[index];
AddedScanDirs[index] = new KeyValuePair<string, SaveLocation>(existing.Key, SaveLocation.Create(value));
}
protected void AddNewScanDir()
{
AddDefaultScanDir();
}
protected async Task ExcludedFileNamesEnabledChanged(bool value)
{
ExcludedFileNamesEnabled = value;
UpdatePreferences.ExcludedFileNamesEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task ExcludedFileNamesChanged(string value)
{
ExcludedFileNames = value;
UpdatePreferences.ExcludedFileNames = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationEnabledChanged(bool value)
{
MailNotificationEnabled = value;
UpdatePreferences.MailNotificationEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationSenderChanged(string value)
{
MailNotificationSender = value;
UpdatePreferences.MailNotificationSender = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationEmailChanged(string value)
{
MailNotificationEmail = value;
UpdatePreferences.MailNotificationEmail = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationSmtpChanged(string value)
{
MailNotificationSmtp = value;
UpdatePreferences.MailNotificationSmtp = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationSslEnabledChanged(bool value)
{
MailNotificationSslEnabled = value;
UpdatePreferences.MailNotificationSslEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationAuthEnabledChanged(bool value)
{
MailNotificationAuthEnabled = value;
UpdatePreferences.MailNotificationAuthEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationUsernameChanged(string value)
{
MailNotificationUsername = value;
UpdatePreferences.MailNotificationUsername = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MailNotificationPasswordChanged(string value)
{
MailNotificationPassword = value;
UpdatePreferences.MailNotificationPassword = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutorunOnTorrentAddedEnabledChanged(bool value)
{
AutorunOnTorrentAddedEnabled = value;
UpdatePreferences.AutorunOnTorrentAddedEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutorunOnTorrentAddedProgramChanged(string value)
{
AutorunOnTorrentAddedProgram = value;
UpdatePreferences.AutorunOnTorrentAddedProgram = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutorunEnabledChanged(bool value)
{
AutorunEnabled = value;
UpdatePreferences.AutorunEnabled = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AutorunProgramChanged(string value)
{
AutorunProgram = value;
UpdatePreferences.AutorunProgram = value;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
private void AddDefaultScanDir()
{
AddedScanDirs.Add(new KeyValuePair<string, SaveLocation>("", SaveLocation.Create(1)));
}
protected Func<string, string?> IsValidNewKey => IsValidNewKeyFunc;
private string? IsValidNewKeyFunc(string? key)
{
if (key is null)
{
return null;
}
if (ScanDirs.ContainsKey(key))
{
return "A folder with this path already exists";
}
return null;
}
}
}

View File

@@ -0,0 +1,38 @@
using Lantean.QBitTorrentClient.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Components.Options
{
public abstract class Options : ComponentBase
{
private bool _preferencesRead;
protected UpdatePreferences UpdatePreferences { get; set; } = new UpdatePreferences();
[Parameter]
[EditorRequired]
public Preferences? Preferences { get; set; }
[Parameter]
[EditorRequired]
public EventCallback<UpdatePreferences> PreferencesChanged { get; set; }
public async Task ResetAsync()
{
SetOptions();
await InvokeAsync(StateHasChanged);
}
protected override void OnParametersSet()
{
if (_preferencesRead)
{
return;
}
_preferencesRead = SetOptions();
}
protected abstract bool SetOptions();
}
}

View File

@@ -0,0 +1,14 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6"></MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,10 @@
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class RSSOptions : Options
{
protected override bool SetOptions()
{
return true;
}
}
}

View File

@@ -0,0 +1,14 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6"></MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,10 @@
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class SpeedOptions : Options
{
protected override bool SetOptions()
{
return true;
}
}
}

View File

@@ -0,0 +1,14 @@
@inherits Options
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4">
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6"></MudText>
</CardHeaderContent>
</MudCardHeader>
<MudCardContent Class="pt-0">
<MudGrid>
</MudGrid>
</MudCardContent>
</MudCard>

View File

@@ -0,0 +1,10 @@
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class WebUIOptions : Options
{
protected override bool SetOptions()
{
return true;
}
}
}

View File

@@ -0,0 +1,32 @@
<MudTable T="Peer" Items="Peers" >
<HeaderContent>
<MudTh>Country/Region</MudTh>
<MudTh>IP</MudTh>
<MudTh>Port</MudTh>
<MudTh>Connection</MudTh>
<MudTh>Flags</MudTh>
<MudTh>Client</MudTh>
<MudTh>Progress</MudTh>
<MudTh>Download Speed</MudTh>
<MudTh>Upload Speed</MudTh>
<MudTh>Downloaded</MudTh>
<MudTh>Uploaded</MudTh>
<MudTh>Relevance</MudTh>
<MudTh>Files</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Country/Region"><MudImage ObjectFit="ObjectFit.Fill" Src="@($"https://flagcdn.com/20x15/{context.CountryCode}.png")"></MudImage></MudTd>
<MudTd DataLabel="IP">@context.IPAddress</MudTd>
<MudTd DataLabel="Port">@context.Port</MudTd>
<MudTd DataLabel="Connection">@context.Connection</MudTd>
<MudTd DataLabel="Flags">@context.Flags</MudTd>
<MudTd DataLabel="Client">@context.Client</MudTd>
<MudTd DataLabel="Progress">@DisplayHelpers.Percentage(context.Progress)</MudTd>
<MudTd DataLabel="Download Speed">@DisplayHelpers.Speed(context.DownloadSpeed)</MudTd>
<MudTd DataLabel="Upload Speed">@DisplayHelpers.Speed(context.UploadSpeed)</MudTd>
<MudTd DataLabel="Downloaded">@DisplayHelpers.Size(context.Downloaded)</MudTd>
<MudTd DataLabel="Uploaded">@DisplayHelpers.Size(context.Uploaded)</MudTd>
<MudTd DataLabel="Relevance">@DisplayHelpers.Percentage(context.Relevance)</MudTd>
<MudTd DataLabel="Files">@context.Files</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -0,0 +1,119 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using System.Net;
namespace Lantean.QBTMudBlade.Components
{
public partial class PeersTab : IAsyncDisposable
{
private bool _disposedValue;
[Parameter, EditorRequired]
public string? Hash { get; set; }
[Parameter]
public bool Active { get; set; }
[CascadingParameter]
public int RefreshInterval { get; set; }
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
protected PeerList? PeerList { get; set; }
protected IEnumerable<Peer> Peers => PeerList?.Peers.Select(p => p.Value) ?? [];
private int _requestId = 0;
private readonly CancellationTokenSource _timerCancellationToken = new();
protected override async Task OnParametersSetAsync()
{
if (Hash is null)
{
return;
}
if (!Active)
{
return;
}
var torrentPeers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
PeerList = DataManager.CreatePeerList(torrentPeers);
_requestId = torrentPeers.RequestId;
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (Hash is null)
{
return;
}
if (Active)
{
QBitTorrentClient.Models.TorrentPeers peers;
try
{
peers = await ApiClient.GetTorrentPeersData(Hash, _requestId);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
if (PeerList is null || peers.FullUpdate)
{
PeerList = DataManager.CreatePeerList(peers);
}
else
{
DataManager.MergeTorrentPeers(peers, PeerList);
}
_requestId = peers.RequestId;
}
await InvokeAsync(StateHasChanged);
}
}
}
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.Delay(0);
}
_disposedValue = true;
}
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,88 @@
@if (Type == ParentType.StandaloneToolbar)
{
<MudToolBar Dense="true" DisableGutters="true">
@ToolbarContent
</MudToolBar>
}
else if (Type == ParentType.Toolbar)
{
@ToolbarContent
}
else
{
<MudMenu AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var option in GetOptions())
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback">
@if (option is Divider)
{
<MudDivider />
}
else if (!option.Children.Any())
{
@option.Name
}
else
{
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
<ActivatorContent>
<MudMenuItem>@option.Name</MudMenuItem>
</ActivatorContent>
<ChildContent>
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</ChildContent>
</MudMenu>
}
</MudMenuItem>
}
</MudMenu>
}
@code {
private RenderFragment ToolbarContent =>
@<NonRendering>
@foreach (var option in GetOptions())
{
@if (option is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
{
if (option.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback">@option.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" />
}
}
else
{
<MudMenu Icon="@option.Icon" IconColor="@option.Color" Label="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</MudMenu>
}
}
</NonRendering>;
private RenderFragment ChildItem(Action option) =>
@<NonRendering>
@if (option is Divider)
{
<MudDivider />
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback">@option.Name</MudMenuItem>
}
</NonRendering>;
}

View File

@@ -0,0 +1,325 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
public partial class TorrentActions
{
[Inject]
public IApiClient ApiClient { get; set; } = default!;
[Inject]
public NavigationManager NavigationManager { get; set; } = default!;
[Inject]
public IDialogService DialogService { get; set; } = default!;
[Inject]
public ISnackbar Snackbar { get; set; } = default!;
[Inject]
public IDataManager DataManager { get; set; } = default!;
[Inject]
public IClipboardService ClipboardService { get; set; } = default!;
[Parameter]
[EditorRequired]
public IEnumerable<string> Hashes { get; set; } = default!;
/// <summary>
/// If true this component will render as a <see cref="MudToolBar"/> otherwise will render as a <see cref="MudMenu"/>.
/// </summary>
[Parameter]
public ParentType Type { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
protected async Task Pause()
{
await ApiClient.PauseTorrents(Hashes);
Snackbar.Add("Torrent paused.");
}
protected async Task Resume()
{
await ApiClient.ResumeTorrents(Hashes);
Snackbar.Add("Torrent resumed.");
}
protected async Task Remove()
{
await DialogService.InvokeDeleteTorrentDialog(ApiClient, Hashes.ToArray());
}
protected async Task SetLocation()
{
string? savePath = null;
if (Hashes.Any() && MainData.Torrents.TryGetValue(Hashes.First(), out var torrent))
{
savePath = torrent.SavePath;
}
await DialogService.ShowSingleFieldDialog("Set Location", "Location", savePath, v => ApiClient.SetTorrentLocation(v, null, Hashes.ToArray()));
}
protected async Task Rename()
{
string? name = null;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
{
name = torrent.Name;
}
await DialogService.ShowSingleFieldDialog("Rename", "Location", name, v => ApiClient.SetTorrentName(v, hash));
}
protected async Task RenameFiles()
{
await DialogService.InvokeRenameFilesDialog(ApiClient, Hashes.First());
}
protected async Task SetCategory(string category)
{
await ApiClient.SetTorrentCategory(category, null, Hashes.ToArray());
}
protected async Task AddCategory()
{
await DialogService.InvokeAddCategoryDialog(ApiClient, Hashes);
}
protected async Task ResetCategory()
{
await ApiClient.SetTorrentCategory("", null, Hashes.ToArray());
}
protected async Task AddTag()
{
await DialogService.ShowSingleFieldDialog("Add Tags", "Comma-separated tags", "", v => ApiClient.AddTorrentTags(v.Split(',', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries), null, Hashes.ToArray()));
}
protected async Task RemoveTags()
{
var torrents = GetTorrents();
foreach (var torrent in torrents)
{
await ApiClient.RemoveTorrentTags(torrent.Tags, null, torrent.Hash);
}
}
protected async Task ToggleTag(string tag)
{
var torrents = GetTorrents();
await ApiClient.RemoveTorrentTag(tag, torrents.Where(t => t.Tags.Contains(tag)).Select(t => t.Hash));
await ApiClient.AddTorrentTag(tag, torrents.Where(t => !t.Tags.Contains(tag)).Select(t => t.Hash));
}
protected async Task ToggleAutoTMM()
{
var torrents = GetTorrents();
await ApiClient.SetAutomaticTorrentManagement(false, null, torrents.Where(t => t.AutomaticTorrentManagement).Select(t => t.Hash).ToArray());
await ApiClient.SetAutomaticTorrentManagement(true, null, torrents.Where(t => !t.AutomaticTorrentManagement).Select(t => t.Hash).ToArray());
}
protected async Task LimitUploadRate()
{
long uploadLimit = -1;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
{
uploadLimit = torrent.UploadLimit;
}
await DialogService.InvokeUploadRateDialog(ApiClient, uploadLimit, Hashes);
}
protected async Task LimitShareRatio()
{
float ratioLimit = -1;
string hash = Hashes.First();
if (Hashes.Any() && MainData.Torrents.TryGetValue(hash, out var torrent))
{
ratioLimit = torrent.RatioLimit;
}
await DialogService.InvokeShareRatioDialog(ApiClient, ratioLimit, Hashes);
}
protected async Task ToggleSuperSeeding()
{
var torrents = GetTorrents();
await ApiClient.SetSuperSeeding(false, null, torrents.Where(t => t.SuperSeeding).Select(t => t.Hash).ToArray());
await ApiClient.SetSuperSeeding(true, null, torrents.Where(t => !t.SuperSeeding).Select(t => t.Hash).ToArray());
}
protected async Task ForceRecheck()
{
await ApiClient.RecheckTorrents(null, Hashes.ToArray());
}
protected async Task ForceReannounce()
{
await ApiClient.ReannounceTorrents(null, Hashes.ToArray());
}
protected async Task MoveToTop()
{
await ApiClient.MaximalTorrentPriority(null, Hashes.ToArray());
}
protected async Task MoveUp()
{
await ApiClient.IncreaseTorrentPriority(null, Hashes.ToArray());
}
protected async Task MoveDown()
{
await ApiClient.DecreaseTorrentPriority(null, Hashes.ToArray());
}
protected async Task MoveToBottom()
{
await ApiClient.MinimalTorrentPriority(null, Hashes.ToArray());
}
protected async Task Copy(string value)
{
await ClipboardService.WriteToClipboard(value);
}
protected async Task Export()
{
await Task.Delay(5);
}
private IEnumerable<Torrent> GetTorrents()
{
foreach (var hash in Hashes)
{
if (MainData.Torrents.TryGetValue(hash, out var torrent))
{
yield return torrent;
}
}
}
private IEnumerable<Action> GetOptions()
{
if (!Hashes.Any())
{
return [];
}
var firstTorrent = MainData.Torrents[Hashes.First()];
var categories = new List<Action>
{
new Action("New", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddCategory)),
new Action("Reset", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, ResetCategory)),
new Divider()
};
categories.AddRange(MainData.Categories.Select(c => new Action(c.Value.Name, Icons.Material.Filled.List, Color.Info, EventCallback.Factory.Create(this, () => SetCategory(c.Key)))));
var tags = new List<Action>
{
new Action("Add", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddTag)),
new Action("Remove All", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, RemoveTags)),
new Divider()
};
tags.AddRange(MainData.Tags.Select(t => new Action(t, firstTorrent.Tags.Contains(t) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, EventCallback.Factory.Create(this, () => ToggleTag(t)))));
var options = new List<Action>
{
new Action("Pause", Icons.Material.Filled.Pause, Color.Warning, EventCallback.Factory.Create(this, Pause)),
new Action("Resume", Icons.Material.Filled.PlayArrow, Color.Success, EventCallback.Factory.Create(this, Resume)),
new Divider(),
new Action("Remove", Icons.Material.Filled.Delete, Color.Error, EventCallback.Factory.Create(this, Remove)),
new Divider(),
new Action("Set location", Icons.Material.Filled.MyLocation, Color.Info, EventCallback.Factory.Create(this, SetLocation)),
new Action("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, EventCallback.Factory.Create(this, Rename)),
new Action("Category", Icons.Material.Filled.List, Color.Info, categories),
new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags),
new Action("Automatic Torrent Management", Icons.Material.Filled.Check, firstTorrent.AutomaticTorrentManagement ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)),
new Divider(),
new Action("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, EventCallback.Factory.Create(this, LimitUploadRate)),
new Action("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, EventCallback.Factory.Create(this, LimitShareRatio)),
new Action("Super seeding mode", Icons.Material.Filled.Check, firstTorrent.SuperSeeding ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleSuperSeeding)),
new Divider(),
new Action("Force recheck", Icons.Material.Filled.Loop, Color.Info, EventCallback.Factory.Create(this, ForceRecheck)),
new Action("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, EventCallback.Factory.Create(this, ForceReannounce)),
new Divider(),
new Action("Queue", Icons.Material.Filled.Queue, Color.Transparent, new List<Action>
{
new Action("Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, EventCallback.Factory.Create(this, MoveToTop)),
new Action("Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, EventCallback.Factory.Create(this, MoveUp)),
new Action("Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, EventCallback.Factory.Create(this, MoveDown)),
new Action("Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, EventCallback.Factory.Create(this, MoveToBottom)),
}),
new Action("Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List<Action>
{
new Action("Name", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.Name))),
new Action("Info hash v1", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.InfoHashV1))),
new Action("Info hash v2", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.InfoHashV2))),
new Action("Magnet link", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.MagnetUri))),
new Action("Torrent ID", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(firstTorrent.Hash))),
}),
new Action("Export", Icons.Material.Filled.SaveAlt, Color.Info, EventCallback.Factory.Create(this, Export)),
};
return options;
}
}
public enum ParentType
{
Toolbar,
StandaloneToolbar,
Menu,
}
public class Divider : Action
{
public Divider() : base("-", default!, Color.Default, default(EventCallback))
{
}
}
public class Action
{
public Action(string name, string icon, Color color, EventCallback callback)
{
Name = name;
Icon = icon;
Color = color;
Callback = callback;
Children = [];
}
public Action(string name, string icon, Color color, IEnumerable<Action> children)
{
Name = name;
Icon = icon;
Color = color;
Callback = default;
Children = children;
}
public string Name { get; }
public string Icon { get; }
public Color Color { get; }
public EventCallback Callback { get; }
public IEnumerable<Action> Children { get; }
}
}

View File

@@ -0,0 +1,18 @@
<MudNavMenu>
<MudNavLink Icon="@(Icons.Material.Outlined.NavigateBefore)" OnClick="NavigateBack">Back</MudNavLink>
<MudDivider />
@if (Torrents is null)
{
@for (var i = 0; i < 10; i++)
{
<MudSkeleton Animation="Animation.Pulse" Width="100%" Height="25px" />
}
}
else
{
foreach (var torrent in Torrents)
{
<MudNavLink Href="@("/details/" + torrent.Hash)">@torrent.Name</MudNavLink>
}
}
</MudNavMenu>

View File

@@ -0,0 +1,22 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Components
{
public partial class TorrentsListNav
{
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Parameter]
public IEnumerable<Torrent>? Torrents { get; set; }
[Parameter]
public string? SelectedTorrent { get; set; }
protected void NavigateBack()
{
NavigationManager.NavigateTo("/");
}
}
}

View File

@@ -0,0 +1,27 @@
<MudTable T="Lantean.QBitTorrentClient.Models.TorrentTrackers" Items="Trackers" >
<HeaderContent>
<MudTh>Tier</MudTh>
<MudTh>URL</MudTh>
<MudTh>Status</MudTh>
<MudTh>Peers</MudTh>
<MudTh>Seeds</MudTh>
<MudTh>Leeches</MudTh>
<MudTh>Times Downloaded</MudTh>
<MudTh>Message</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Tier">
@if (context.Tier >= 0)
{
<text>@context.Tier</text>
}
</MudTd>
<MudTd DataLabel="URL">@context.Url</MudTd>
<MudTd DataLabel="Status">@context.Status</MudTd>
<MudTd DataLabel="Peers">@context.Peers</MudTd>
<MudTd DataLabel="Seeds">@context.Seeds</MudTd>
<MudTd DataLabel="Leeches">@context.Leeches</MudTd>
<MudTd DataLabel="Times Downloaded">@context.Downloads</MudTd>
<MudTd DataLabel="Message">@context.Message</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -0,0 +1,97 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using System.Net;
namespace Lantean.QBTMudBlade.Components
{
public partial class TrackersTab : IAsyncDisposable
{
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
[Parameter, EditorRequired]
public string? Hash { get; set; }
[Parameter]
public bool Active { get; set; }
[CascadingParameter]
public int RefreshInterval { get; set; }
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
protected IReadOnlyList<TorrentTrackers>? Trackers { get; set; }
protected override async Task OnParametersSetAsync()
{
if (Hash is null)
{
return;
}
if (!Active)
{
return;
}
Trackers = await ApiClient.GetTorrentTrackers(Hash);
await InvokeAsync(StateHasChanged);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (Active && Hash is not null)
{
try
{
Trackers = await ApiClient.GetTorrentTrackers(Hash);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
}
await InvokeAsync(StateHasChanged);
}
}
}
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.Delay(0);
}
_disposedValue = true;
}
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,8 @@
<MudTable T="Lantean.QBitTorrentClient.Models.WebSeed" Items="WebSeeds" >
<HeaderContent>
<MudTh>URL</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Files">@context.Url</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -0,0 +1,92 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using System.Net;
namespace Lantean.QBTMudBlade.Components
{
public partial class WebSeedsTab : IAsyncDisposable
{
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
[Parameter]
public bool Active { get; set; }
[Parameter, EditorRequired]
public string? Hash { get; set; }
[CascadingParameter]
public int RefreshInterval { get; set; }
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
protected IReadOnlyList<WebSeed>? WebSeeds { get; set; }
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.Delay(0);
}
_disposedValue = true;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (Active && Hash is not null)
{
try
{
WebSeeds = await ApiClient.GetTorrentWebSeeds(Hash);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
}
await InvokeAsync(StateHasChanged);
}
}
}
}
protected override async Task OnParametersSetAsync()
{
if (Hash is null)
{
return;
}
WebSeeds = await ApiClient.GetTorrentWebSeeds(Hash);
await InvokeAsync(StateHasChanged);
}
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components.WebAssembly.Http;
namespace Lantean.QBTMudBlade
{
public class CookieHandler : DelegatingHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include);
return await base.SendAsync(request, cancellationToken);
}
}
}

View File

@@ -0,0 +1,7 @@
namespace Lantean.QBTMudBlade
{
public static class CustomIcons
{
public const string Magnet = @"<path fill=""currentColor"" d=""M3 7v6a9 9 0 0 0 9 9a9 9 0 0 0 9-9V7h-4v6a5 5 0 0 1-5 5a5 5 0 0 1-5-5V7m10-2h4V2h-4M3 5h4V2H3""/>";
}
}

View File

@@ -0,0 +1,229 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Filter;
using Lantean.QBTMudBlade.Models;
using MudBlazor;
namespace Lantean.QBTMudBlade
{
public static class DialogHelper
{
public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, ClassBackground = "background-blur" };
private static readonly DialogOptions _confirmDialogOptions = new() { ClassBackground = "background-blur" };
public static async Task InvokeAddTorrentFileDialog(this IDialogService dialogService, IApiClient apiClient)
{
var result = await dialogService.ShowAsync<AddTorrentFileDialog>("Upload local torrent", FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
var options = (AddTorrentFileOptions)dialogResult.Data;
var streams = new List<Stream>();
var files = new Dictionary<string, Stream>();
foreach (var file in options.Files)
{
var stream = file.OpenReadStream();
streams.Add(stream);
files.Add(file.Name, stream);
}
await apiClient.AddTorrent(
urls: null,
files,
options.SavePath,
options.Cookie,
options.Category,
tags: null,
options.SkipHashCheck,
!options.StartTorrent,
options.ContentLayout,
options.RenameTorrent,
options.UploadLimit,
options.DownloadLimit,
ratioLimit: null,
seedingTimeLimit: null,
options.TorrentManagementMode,
options.DownloadInSequentialOrder,
options.DownloadFirstAndLastPiecesFirst);
foreach (var stream in streams)
{
await stream.DisposeAsync();
}
}
public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient)
{
var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
var options = (AddTorrentLinkOptions)dialogResult.Data;
await apiClient.AddTorrent(
urls: options.Urls,
torrents: null,
options.SavePath,
options.Cookie,
options.Category,
tags: null,
options.SkipHashCheck,
!options.StartTorrent,
options.ContentLayout,
options.RenameTorrent,
options.UploadLimit,
options.DownloadLimit,
ratioLimit: null,
seedingTimeLimit: null,
options.TorrentManagementMode,
options.DownloadInSequentialOrder,
options.DownloadFirstAndLastPiecesFirst);
}
public static async Task InvokeDeleteTorrentDialog(this IDialogService dialogService, IApiClient apiClient, params string[] hashes)
{
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?");
var result = await reference.Result;
if (result.Canceled)
{
return;
}
await apiClient.DeleteTorrents(hashes, (bool)result.Data);
}
public static async Task InvokeRenameFilesDialog(this IDialogService dialogService, IApiClient apiClient, string hash)
{
await Task.Delay(0);
}
public static async Task InvokeAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient, IEnumerable<string>? hashes = null)
{
var reference = await dialogService.ShowAsync<DeleteDialog>("New Category");
var result = await reference.Result;
if (result.Canceled)
{
return;
}
var category = (Category)result.Data;
await apiClient.AddCategory(category.Name, category.SavePath);
if (hashes is not null)
{
await apiClient.SetTorrentCategory(category.Name, null, hashes.ToArray());
}
}
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func<Task> onSuccess)
{
var parameters = new DialogParameters
{
{ nameof(ConfirmDialog.Content), content }
};
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, _confirmDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
await onSuccess();
}
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Action onSuccess)
{
await ShowConfirmDialog(dialogService, title, content, () =>
{
onSuccess();
return Task.CompletedTask;
});
}
public static async Task ShowSingleFieldDialog<T>(this IDialogService dialogService, string title, string label, T? value, Func<T, Task> onSuccess)
{
var parameters = new DialogParameters
{
{ nameof(SingleFieldDialog<T>.Label), label },
{ nameof(SingleFieldDialog<T>.Value), value }
};
var result = await dialogService.ShowAsync<SingleFieldDialog<T>>(title, parameters, _confirmDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
await onSuccess((T)dialogResult.Data);
}
public static async Task InvokeUploadRateDialog(this IDialogService dialogService, IApiClient apiClient, long rate, IEnumerable<string> hashes)
{
var parameters = new DialogParameters
{
{ nameof(SliderFieldDialog<long>.Value), rate },
{ nameof(SliderFieldDialog<long>.Min), 0 },
{ nameof(SliderFieldDialog<long>.Max), 100 },
};
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
await apiClient.SetTorrentUploadLimit((long)dialogResult.Data, null, hashes.ToArray());
}
public static async Task InvokeShareRatioDialog(this IDialogService dialogService, IApiClient apiClient, float ratio, IEnumerable<string> hashes)
{
var parameters = new DialogParameters
{
{ nameof(SliderFieldDialog<float>.Value), ratio },
{ nameof(SliderFieldDialog<float>.Min), 0 },
{ nameof(SliderFieldDialog<float>.Max), 100 },
};
var result = await dialogService.ShowAsync<SliderFieldDialog<float>>("Upload Rate", parameters, FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return;
}
await apiClient.SetTorrentShareLimit((float)dialogResult.Data, 0, null, hashes.ToArray());
}
public static async Task<List<PropertyFilterDefinition<T>>?> ShowFilterOptionsDialog<T>(this IDialogService dialogService, List<PropertyFilterDefinition<T>>? propertyFilterDefinitions)
{
var parameters = new DialogParameters
{
{ nameof(FilterOptionsDialog<T>.FilterDefinitions), propertyFilterDefinitions },
};
var result = await dialogService.ShowAsync<FilterOptionsDialog<T>>("Filters", parameters, FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult.Canceled)
{
return null;
}
return (List<PropertyFilterDefinition<T>>?)dialogResult.Data;
}
}
}

View File

@@ -0,0 +1,379 @@
using ByteSizeLib;
using Lantean.QBTMudBlade.Models;
using MudBlazor;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using System.Text;
namespace Lantean.QBTMudBlade
{
public static class DisplayHelpers
{
/// <summary>
/// Formats a time period in seconds into an appropriate unit based on the size.
/// </summary>
/// <param name="seconds"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string Duration(long? seconds, string? prefix = null, string? suffix = null)
{
if (seconds is null)
{
return "";
}
if (seconds == 8640000)
{
return "∞";
}
var time = TimeSpan.FromSeconds(seconds.Value);
var sb = new StringBuilder();
if (prefix is not null)
{
sb.Append(prefix);
}
if (time.Days > 0)
{
sb.Append(time.Days);
sb.Append('d');
if (time.Hours != 0)
{
sb.Append(' ');
sb.Append(time.Hours);
sb.Append('h');
}
}
else if (time.Hours > 0)
{
sb.Append(time.Hours);
sb.Append('h');
if (time.Minutes != 0)
{
sb.Append(' ');
sb.Append(time.Minutes);
sb.Append('m');
}
}
else
{
sb.Append(time.Minutes);
sb.Append('m');
}
if (suffix is not null)
{
sb.Append(' ');
sb.Append(suffix);
}
return sb.ToString();
}
/// <summary>
/// Formats a file size in bytes into an appropriate unit based on the size.
/// </summary>
/// <param name="size"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string Size(long? size, string? prefix = null, string? suffix = null)
{
if (size is null)
{
return "";
}
var stringBuilder = new StringBuilder();
if (prefix is not null)
{
stringBuilder.Append(prefix);
}
stringBuilder.Append(ByteSize.FromBytes(size.Value).ToString("#.##"));
if (suffix is not null)
{
stringBuilder.Append(suffix);
}
return stringBuilder.ToString();
}
/// <summary>
/// Formats a file size in bytes into an appropriate unit based on the size.
/// </summary>
/// <param name="size"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string Size(object? sizeValue, string? prefix = null, string? suffix = null)
{
if (sizeValue is not long size)
{
return "";
}
return Size(size);
}
/// <summary>
/// Formats a transfer speed in bytes/s into an appropriate unit based on the size.
/// </summary>
/// <param name="size"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string Speed(long? size, string? prefix = null, string? suffix = null)
{
if (size is null)
{
return "";
}
if (size == -1)
{
return "∞";
}
var stringBuilder = new StringBuilder();
if (prefix is not null)
{
stringBuilder.Append(prefix);
}
stringBuilder.Append(ByteSize.FromBytes(size.Value).ToString("#.##"));
stringBuilder.Append("/s");
if (suffix is not null)
{
stringBuilder.Append(suffix);
}
return stringBuilder.ToString();
}
/// <summary>
/// Formats a value into an empty string if null, otherwise the value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string EmptyIfNull<T>(T? value, string? prefix = null, string? suffix = null, [StringSyntax("NumericFormat")] string? format = null) where T : struct, IConvertible
{
if (value is null)
{
return "";
}
var stringBuilder = new StringBuilder();
if (prefix is not null)
{
stringBuilder.Append(prefix);
}
if (format is not null)
{
if (value is long longValue)
{
stringBuilder.Append(longValue.ToString(format));
}
else if (value is int intValue)
{
stringBuilder.Append(intValue.ToString(format));
}
else if (value is float floatValue)
{
stringBuilder.Append(floatValue.ToString(format));
}
else if (value is double doubleValue)
{
stringBuilder.Append(doubleValue.ToString(format));
}
else if (value is decimal decimalValue)
{
stringBuilder.Append(decimalValue.ToString(format));
}
else if (value is short shortValue)
{
stringBuilder.Append(shortValue.ToString(format));
}
else
{
stringBuilder.Append(value.Value);
}
}
else
{
stringBuilder.Append(value.Value);
}
if (suffix is not null)
{
stringBuilder.Append(suffix);
}
return stringBuilder.ToString();
}
/// <summary>
/// Formats a value into an empty string if null, otherwise the value.
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="value"></param>
/// <param name="prefix"></param>
/// <param name="suffix"></param>
/// <returns></returns>
public static string EmptyIfNull(string? value, string? prefix = null, string? suffix = null)
{
if (value is null)
{
return "";
}
var stringBuilder = new StringBuilder();
if (prefix is not null)
{
stringBuilder.Append(prefix);
}
stringBuilder.Append(value);
if (suffix is not null)
{
stringBuilder.Append(suffix);
}
return stringBuilder.ToString();
}
/// <summary>
/// Formats a unix time (in seconds) into a local date time.
/// </summary>
/// <param name="value"></param>
/// <param name="negativeDescription"></param>
/// <returns></returns>
public static string DateTime(long? value, string negativeDescription = "")
{
if (value is null)
{
return "";
}
if (value.Value == -1)
{
return negativeDescription;
}
var dateTimeOffset = DateTimeOffset.FromUnixTimeSeconds(value.Value);
return dateTimeOffset.ToLocalTime().ToString();
}
/// <summary>
/// Formats a value into a percentage or empty string if null.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public static string Percentage(float? value)
{
if (value is null)
{
return "";
}
if (value == 0)
{
return "0%";
}
return value.Value.ToString("0.#%");
}
public static string State(string? state)
{
var status = state switch
{
"downloading" => "Downloading",
"stalledDL" => "Stalled",
"metaDL" => "Downloading metadata",
"forcedMetaDL" => "[F] Downloading metadata",
"forcedDL" => "[F] Downloading",
"uploading" or "stalledUP" => "Seeding",
"forcedUP" => "[F] Seeding",
"queuedDL" or "queuedUP" => "Queued",
"checkingDL" or "checkingUP" => "Checking",
"queuedForChecking" => "Queued for checking",
"checkingResumeData" => "Checking resume data",
"pausedDL" => "Paused",
"pausedUP" => "Completed",
"moving" => "Moving",
"missingFiles" => "Missing Files",
"error" => "Errored",
_ => "Unknown",
};
return status;
}
public static (string, Color) GetStateIcon(string? state)
{
switch (state)
{
case "forcedDL":
case "metaDL":
case "forcedMetaDL":
case "downloading":
return (Icons.Material.Filled.Downloading, Color.Success);
case "forcedUP":
case "uploading":
return (Icons.Material.Filled.Upload, Color.Info);
case "stalledUP":
return (Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info);
case "stalledDL":
return (Icons.Material.Filled.KeyboardDoubleArrowDown, Color.Success);
case "pausedDL":
return (Icons.Material.Filled.Pause, Color.Success);
case "pausedUP":
return (Icons.Material.Filled.Pause, Color.Info);
case "queuedDL":
case "queuedUP":
return (Icons.Material.Filled.Queue, Color.Default);
case "checkingDL":
case "checkingUP":
return (Icons.Material.Filled.Loop, Color.Info);
case "queuedForChecking":
case "checkingResumeData":
return (Icons.Material.Filled.Loop, Color.Warning);
case "moving":
return (Icons.Material.Filled.Moving, Color.Info);
case "error":
case "unknown":
case "missingFiles":
return (Icons.Material.Filled.Error, Color.Error);
default:
return (Icons.Material.Filled.QuestionMark, Color.Warning);
}
}
public static (string, Color) GetStatusIcon(string statusValue)
{
var status = Enum.Parse<Status>(statusValue);
return GetStatusIcon(status);
}
private static (string, Color) GetStatusIcon(Status status)
{
return status switch
{
Status.All => (Icons.Material.Filled.AllOut, Color.Warning),
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.Active => (Icons.Material.Filled.Sort, Color.Success),
Status.Inactive => (Icons.Material.Filled.Sort, Color.Error),
Status.Stalled => (Icons.Material.Filled.Sort, Color.Info),
Status.StalledUploading => (Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info),
Status.StalledDownloading => (Icons.Material.Filled.KeyboardDoubleArrowDown, Color.Success),
Status.Checking => (Icons.Material.Filled.Loop, Color.Info),
Status.Errored => (Icons.Material.Filled.Error, Color.Error),
_ => (Icons.Material.Filled.QuestionMark, Color.Inherit),
};
}
}
}

View File

@@ -0,0 +1,154 @@
using System.Diagnostics.CodeAnalysis;
using System.Linq.Expressions;
namespace Lantean.QBTMudBlade
{
internal static class ExpressionModifier
{
internal static Expression<Func<T, bool>> Modify<T>(this Expression firstExpression, Expression secondExpression)
{
var bodyIdentifier = new ExpressionBodyIdentifier();
var body = bodyIdentifier.Identify(firstExpression);
var parameterIdentifier = new ExpressionParameterIdentifier();
var parameter = (ParameterExpression)parameterIdentifier.Identify(firstExpression);
var body2 = bodyIdentifier.Identify(secondExpression);
var parameter2 = (ParameterExpression)parameterIdentifier.Identify(secondExpression);
var treeModifier = new ExpressionReplacer(parameter2, body);
return Expression.Lambda<Func<T, bool>>(treeModifier.Visit(body2), parameter);
}
internal static Expression ReplaceBinary(this Expression exp, ExpressionType from, ExpressionType to)
{
var binaryReplacer = new BinaryReplacer(from, to);
return binaryReplacer.Visit(exp);
}
public static Expression<Func<T, bool>> GenerateBinary<T>(this Expression expression, ExpressionType binaryOperation, object? value)
{
var bodyIdentifier = new ExpressionBodyIdentifier();
var body = bodyIdentifier.Identify(expression);
var parameterIdentifier = new ExpressionParameterIdentifier();
var parameter = (ParameterExpression)parameterIdentifier.Identify(expression);
BinaryExpression? binaryExpression;
if (Nullable.GetUnderlyingType(body.Type) is not null)
{
// property type is nullable...
binaryExpression = Expression.MakeBinary(binaryOperation, body, Expression.Convert(Expression.Constant(value), body.Type));
}
else
{
if (value is null)
{
// We can short circuit here because the value to be compared is null and the property type is not nullable.
return x => true;
}
binaryExpression = Expression.MakeBinary(binaryOperation, body, Expression.Convert(Expression.Constant(value), body.Type));
}
return Expression.Lambda<Func<T, bool>>(binaryExpression, parameter);
}
public static Expression<Func<T, U>> ChangeExpressionReturnType<T, U>(this Expression expression)
{
var bodyIdentifier = new ExpressionBodyIdentifier();
var body = bodyIdentifier.Identify(expression);
var parameterIdentifier = new ExpressionParameterIdentifier();
var parameter = (ParameterExpression)parameterIdentifier.Identify(expression);
if (body.Type is U)
{
// Expression already has the right type.
return Expression.Lambda<Func<T, U>>(body, parameter);
}
// Change parameter.
var converted = Expression.Convert(body, typeof(U));
return Expression.Lambda<Func<T, U>>(converted, parameter);
}
public static (Expression<Func<T, object?>>, Type) CreatePropertySelector<T>(string propertyName)
{
var type = typeof(T);
var propertyInfo = type.GetProperty(propertyName, System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
if (propertyInfo is null)
{
throw new InvalidOperationException($"Unable to match property {propertyName} for {type.Name}");
}
var parameterExpression = Expression.Parameter(type);
var propertyExpression = Expression.Property(parameterExpression, propertyInfo);
var convertExpression = Expression.Convert(propertyExpression, typeof(object));
return (Expression.Lambda<Func<T, object?>>(convertExpression, parameterExpression), propertyInfo.PropertyType);
}
}
internal class ExpressionReplacer : ExpressionVisitor
{
private readonly Expression _from;
private readonly Expression _to;
public ExpressionReplacer(Expression from, Expression to)
{
_from = from;
_to = to;
}
[return: NotNullIfNotNull(nameof(node))]
public override Expression? Visit(Expression? node)
{
if (node == _from) return _to;
return base.Visit(node);
}
}
internal class ExpressionBodyIdentifier : ExpressionVisitor
{
public Expression Identify(Expression node)
{
return base.Visit(node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
return node.Body;
}
}
internal class ExpressionParameterIdentifier : ExpressionVisitor
{
public Expression Identify(Expression node)
{
return base.Visit(node);
}
protected override Expression VisitLambda<T>(Expression<T> node)
{
return node.Parameters[0];
}
}
internal class BinaryReplacer : ExpressionVisitor
{
private readonly ExpressionType _from;
private readonly ExpressionType _to;
public BinaryReplacer(ExpressionType from, ExpressionType to)
{
_from = from;
_to = to;
}
protected override Expression VisitBinary(BinaryExpression node)
{
if (node.NodeType == _from)
{
return Expression.MakeBinary(_to, node.Left, node.Right);
}
return base.VisitBinary(node);
}
}
}

View File

@@ -0,0 +1,54 @@
using Lantean.QBTMudBlade;
using Lantean.QBTMudBlade.Models;
namespace Lantean.QBTMudBlade
{
public static class Extensions
{
public const char DirectorySeparator = '/';
public static string GetDirectoryPath(this string pathAndFileName)
{
return string.Join(DirectorySeparator, pathAndFileName.Split(DirectorySeparator)[..^1]);
}
public static string GetDirectoryPath(this ContentItem contentItem)
{
return contentItem.Name.GetDirectoryPath();
}
public static string GetFileName(this string pathAndFileName)
{
return pathAndFileName.Split(DirectorySeparator)[^1];
}
public static string GetFileName(this ContentItem contentItem)
{
return contentItem.Name.GetFileName();
}
public static string GetDescendantsKey(this string pathAndFileName, int? level = null)
{
var paths = pathAndFileName.Split(DirectorySeparator);
var index = level is null ? new Index(1, true) : new Index(level.Value);
return string.Join(DirectorySeparator, paths[0..index]) + DirectorySeparator;
}
public static string GetDescendantsKey(this ContentItem contentItem, int? level = null)
{
return contentItem.Name.GetDescendantsKey(level);
}
public static void CancelIfNotDisposed(this CancellationTokenSource cancellationTokenSource)
{
try
{
cancellationTokenSource.Cancel();
}
catch (ObjectDisposedException)
{
// disposed
}
}
}
}

View File

@@ -0,0 +1,134 @@
using MudBlazor;
using System.Linq.Expressions;
namespace Lantean.QBTMudBlade.Filter
{
public static class FilterExpressionGenerator
{
public static Expression<Func<T, bool>> GenerateExpression<T>(PropertyFilterDefinition<T> filter, bool caseSensitive)
{
var propertyExpression = filter.Expression;
if (propertyExpression is null)
{
return x => true;
}
var fieldType = FieldType.Identify(filter.ColumnType);
if (fieldType.IsString)
{
var value = filter.Value?.ToString();
if (value is null && filter.Operator != FilterOperator.String.Empty && filter.Operator != FilterOperator.String.NotEmpty)
{
return x => true;
}
var stringComparer = caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
return filter.Operator switch
{
FilterOperator.String.Contains =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.Contains(value, stringComparer))),
FilterOperator.String.NotContains =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && !x.Contains(value, stringComparer))),
FilterOperator.String.Equal =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && x.Equals(value, stringComparer))),
FilterOperator.String.NotEqual =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && !x.Equals(value, stringComparer))),
FilterOperator.String.StartsWith =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.StartsWith(value, stringComparer))),
FilterOperator.String.EndsWith =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => x != null && value != null && x.EndsWith(value, stringComparer))),
FilterOperator.String.Empty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => string.IsNullOrWhiteSpace(x))),
FilterOperator.String.NotEmpty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => !string.IsNullOrWhiteSpace(x))),
_ => x => true
};
}
if (fieldType.IsNumber)
{
if (filter.Value is null && filter.Operator != FilterOperator.Number.Empty && filter.Operator != FilterOperator.Number.NotEmpty)
{
return x => true;
}
return filter.Operator switch
{
FilterOperator.Number.Equal => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, filter.Value),
FilterOperator.Number.NotEqual => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, filter.Value),
FilterOperator.Number.GreaterThan => propertyExpression.GenerateBinary<T>(ExpressionType.GreaterThan, filter.Value),
FilterOperator.Number.GreaterThanOrEqual => propertyExpression.GenerateBinary<T>(ExpressionType.GreaterThanOrEqual, filter.Value),
FilterOperator.Number.LessThan => propertyExpression.GenerateBinary<T>(ExpressionType.LessThan, filter.Value),
FilterOperator.Number.LessThanOrEqual => propertyExpression.GenerateBinary<T>(ExpressionType.LessThanOrEqual, filter.Value),
FilterOperator.Number.Empty => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, null),
FilterOperator.Number.NotEmpty => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, null),
_ => x => true
};
}
if (fieldType.IsDateTime)
{
if (filter.Value is null && filter.Operator != FilterOperator.DateTime.Empty && filter.Operator != FilterOperator.DateTime.NotEmpty)
{
return x => true;
}
return filter.Operator switch
{
FilterOperator.DateTime.Is => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, filter.Value),
FilterOperator.DateTime.IsNot => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, filter.Value),
FilterOperator.DateTime.After => propertyExpression.GenerateBinary<T>(ExpressionType.GreaterThan, filter.Value),
FilterOperator.DateTime.OnOrAfter => propertyExpression.GenerateBinary<T>(ExpressionType.GreaterThanOrEqual, filter.Value),
FilterOperator.DateTime.Before => propertyExpression.GenerateBinary<T>(ExpressionType.LessThan, filter.Value),
FilterOperator.DateTime.OnOrBefore => propertyExpression.GenerateBinary<T>(ExpressionType.LessThanOrEqual, filter.Value),
FilterOperator.DateTime.Empty => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, null),
FilterOperator.DateTime.NotEmpty => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, null),
_ => x => true
};
}
if (fieldType.IsBoolean)
{
if (filter.Value is null)
{
return x => true;
}
return filter.Operator switch
{
FilterOperator.Boolean.Is => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, filter.Value),
_ => x => true
};
}
if (fieldType.IsEnum)
{
if (filter.Value is null)
{
return x => true;
}
return filter.Operator switch
{
FilterOperator.Enum.Is => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, filter.Value),
FilterOperator.Enum.IsNot => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, filter.Value),
_ => x => true
};
}
if (fieldType.IsGuid)
{
return filter.Operator switch
{
FilterOperator.Guid.Equal => propertyExpression.GenerateBinary<T>(ExpressionType.Equal, filter.Value),
FilterOperator.Guid.NotEqual => propertyExpression.GenerateBinary<T>(ExpressionType.NotEqual, filter.Value),
_ => x => true
};
}
return x => true;
}
}
}

View File

@@ -0,0 +1,137 @@
using MudBlazor;
namespace Lantean.QBTMudBlade.Filter
{
public static class FilterOperator
{
public static class String
{
public const string Contains = "contains";
public const string NotContains = "not contains";
public const string Equal = "equals";
public const string NotEqual = "not equals";
public const string StartsWith = "starts with";
public const string EndsWith = "ends with";
public const string Empty = "is empty";
public const string NotEmpty = "is not empty";
}
public static class Number
{
public const string Equal = "=";
public const string NotEqual = "!=";
public const string GreaterThan = ">";
public const string GreaterThanOrEqual = ">=";
public const string LessThan = "<";
public const string LessThanOrEqual = "<=";
public const string Empty = "is empty";
public const string NotEmpty = "is not empty";
}
public static class Enum
{
public const string Is = "is";
public const string IsNot = "is not";
}
public static class Boolean
{
public const string Is = "is";
}
public static class DateTime
{
public const string Is = "is";
public const string IsNot = "is not";
public const string After = "is after";
public const string OnOrAfter = "is on or after";
public const string Before = "is before";
public const string OnOrBefore = "is on or before";
public const string Empty = "is empty";
public const string NotEmpty = "is not empty";
}
public static class Guid
{
public const string Equal = "equals";
public const string NotEqual = "not equals";
}
internal static string[] GetOperatorByDataType(Type type)
{
var fieldType = FieldType.Identify(type);
return GetOperatorByDataType(fieldType);
}
internal static string[] GetOperatorByDataType(FieldType fieldType)
{
if (fieldType.IsString)
{
return new[]
{
String.Contains,
String.NotContains,
String.Equal,
String.NotEqual,
String.StartsWith,
String.EndsWith,
String.Empty,
String.NotEmpty,
};
}
if (fieldType.IsNumber)
{
return new[]
{
Number.Equal,
Number.NotEqual,
Number.GreaterThan,
Number.GreaterThanOrEqual,
Number.LessThan,
Number.LessThanOrEqual,
Number.Empty,
Number.NotEmpty,
};
}
if (fieldType.IsEnum)
{
return new[] {
Enum.Is,
Enum.IsNot,
};
}
if (fieldType.IsBoolean)
{
return new[]
{
Boolean.Is,
};
}
if (fieldType.IsDateTime)
{
return new[]
{
DateTime.Is,
DateTime.IsNot,
DateTime.After,
DateTime.OnOrAfter,
DateTime.Before,
DateTime.OnOrBefore,
DateTime.Empty,
DateTime.NotEmpty,
};
}
if (fieldType.IsGuid)
{
return new[]
{
Guid.Equal,
Guid.NotEqual,
};
}
// default
return Array.Empty<string>();
}
}
}

View File

@@ -0,0 +1,32 @@
using MudBlazor;
using System.Linq.Expressions;
using System.Reflection;
namespace Lantean.QBTMudBlade.Filter
{
public record PropertyFilterDefinition<T>
{
public PropertyFilterDefinition(string column, string @operator, object? value)
{
var (expression, propertyType) = ExpressionModifier.CreatePropertySelector<T>(column);
Column = column;
ColumnType = propertyType;
Operator = @operator;
Value = value;
Expression = expression;
}
public string Column { get; }
public Type ColumnType { get; }
public string Operator { get; set; }
public object? Value { get; set; }
public Expression<Func<T, object?>> Expression { get; }
}
}

View File

@@ -0,0 +1,282 @@
using Lantean.QBTMudBlade.Models;
namespace Lantean.QBTMudBlade
{
public static class FilterHelper
{
public const string TAG_ALL = "All";
public const string TAG_UNTAGGED = "Untagged";
public const string CATEGORY_ALL = "All";
public const string CATEGORY_UNCATEGORIZED = "Uncategorized";
public const string TRACKER_ALL = "All";
public const string TRACKER_TRACKERLESS = "Trackerless";
public static IEnumerable<Torrent> Filter(this IEnumerable<Torrent> torrents, FilterState filterState)
{
return torrents.Where(t => FilterStatus(t, filterState.Status))
.Where(t => FilterTag(t, filterState.Tag))
.Where(t => FilterCategory(t, filterState.Category, filterState.UseSubcategories))
.Where(t => FilterTracker(t, filterState.Tracker))
.Where(t => FilterTerms(t.Name, filterState.Terms));
}
public static HashSet<string> ToHashesHashSet(this IEnumerable<Torrent> torrents)
{
return torrents.Select(t => t.Hash).ToHashSet();
}
public static bool AddIfTrue(this HashSet<string> hashSet, string value, bool condition)
{
if (condition)
{
return hashSet.Add(value);
}
return false;
}
public static bool RemoveIfTrue(this HashSet<string> hashSet, string value, bool condition)
{
if (condition)
{
return hashSet.Remove(value);
}
return false;
}
public static bool AddIfTrueOrRemove(this HashSet<string> hashSet, string value, bool condition)
{
if (condition)
{
return hashSet.Add(value);
}
else
{
return hashSet.Remove(value);
}
}
public static bool ContainsAllTerms(string text, IEnumerable<string> terms)
{
return terms.Any(t =>
{
var term = t;
var isTermRequired = term[0] == '+';
var isTermExcluded = term[0] == '-';
if (isTermRequired || isTermExcluded)
{
if (term.Length == 1)
{
return true;
}
term = term[1..];
}
var textContainsTerm = text.Contains(term, StringComparison.OrdinalIgnoreCase);
return isTermExcluded ? !textContainsTerm : textContainsTerm;
});
}
public static bool FilterTerms(string field, string? terms)
{
if (terms is null || terms == "")
{
return true;
}
return ContainsAllTerms(field, terms.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}
public static bool FilterTerms(Torrent torrent, string? terms)
{
if (terms is null || terms == "")
{
return true;
}
return ContainsAllTerms(torrent.Name, terms.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
}
public static bool FilterTracker(Torrent torrent, string tracker)
{
if (tracker == TRACKER_ALL)
{
return true;
}
if (tracker == TRACKER_TRACKERLESS)
{
return torrent.Tracker == "";
}
return torrent.Tracker == tracker;
}
public static bool FilterCategory(Torrent torrent, string category, bool useSubcategories)
{
switch (category)
{
case CATEGORY_ALL:
break;
case CATEGORY_UNCATEGORIZED:
if (!string.IsNullOrEmpty(torrent.Category))
{
return false;
}
break;
default:
if (!useSubcategories)
{
if (torrent.Category != category)
{
return false;
}
else
{
if (!torrent.Category.StartsWith(category))
{
return false;
}
}
}
break;
}
return true;
}
public static bool FilterTag(Torrent torrent, string tag)
{
if (tag == TAG_ALL)
{
return true;
}
if (tag == TAG_UNTAGGED)
{
return torrent.Tags.Count == 0;
}
return torrent.Tags.Contains(tag);
}
public static bool FilterStatus(Torrent torrent, Status status)
{
var state = torrent.State;
bool inactive = false;
switch (status)
{
case Status.All:
return true;
case Status.Downloading:
if (state != "downloading" && !state.Contains("DL"))
{
return false;
}
break;
case Status.Seeding:
if (state != "uploading" && state != "forcedUP" && state != "stalledUP" && state != "queuedUP" && state != "checkingUP")
{
return false;
}
break;
case Status.Completed:
if (state != "uploading" && !state.Contains("UL"))
{
return false;
}
break;
case Status.Resumed:
if (!state.Contains("resumed"))
{
return false;
}
break;
case Status.Paused:
if (!state.Contains("paused"))
{
return false;
}
break;
case Status.Inactive:
case Status.Active:
if (status == Status.Inactive)
{
inactive = true;
}
bool check;
if (state == "stalledDL")
{
check = torrent.UploadSpeed > 0;
}
else
{
check = state == "metaDL" || state == "forcedMetaDL" || state == "downloading" || state == "forcedDL" || state == "uploading" || state == "forcedUP";
}
if (check == inactive)
{
return false;
}
break;
case Status.Stalled:
if (state != "stalledUP" && state != "stalledDL")
{
return false;
}
break;
case Status.StalledUploading:
if (state != "stalledUP")
{
return false;
}
break;
case Status.StalledDownloading:
if (state != "stalledDL")
{
return false;
}
break;
case Status.Checking:
if (state != "checkingUP" && state != "checkingDL" && state != "checkingResumeData")
{
return false;
}
break;
case Status.Errored:
if (state != "error" && state != "unknown" && state != "missingFiles")
{
return false;
}
break;
}
return true;
}
public static string GetStatusName(this string status)
{
return status switch
{
nameof(Status.StalledUploading) => "Stalled Uploading",
nameof(Status.StalledDownloading) => "Stalled Downloading",
_ => status,
};
}
}
}

View File

@@ -0,0 +1,24 @@
using Lantean.QBTMudBlade.Models;
namespace Lantean.QBTMudBlade
{
public struct FilterState
{
public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms)
{
Category = category;
Status = status;
Tag = tag;
Tracker = tracker;
UseSubcategories = useSubcategories;
Terms = terms;
}
public string Category { get; } = "all";
public Status Status { get; } = Status.All;
public string Tag { get; } = "all";
public string Tracker { get; } = "all";
public bool UseSubcategories { get; }
public string? Terms { get; }
}
}

View File

@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "<Pending>", Scope = "member", Target = "~M:qtmud.Models.Torrent.#ctor(System.String,System.DateTimeOffset,System.Int64,System.Boolean,System.Single,System.String,System.Int64,System.DateTimeOffset,System.String,System.Int64,System.Int64,System.Int64,System.Int64,System.Int64,System.Boolean,System.Boolean,System.String,System.String,System.DateTimeOffset,System.String,System.Single,System.Int32,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Single,System.String,System.DateTimeOffset,System.Int32,System.DateTimeOffset,System.Boolean,System.Int64,System.String,System.Boolean,System.Collections.Generic.IEnumerable{System.String},System.DateTimeOffset,System.Int64,System.String,System.Int64,System.Int64,System.Int64,System.Int64)")]

View File

@@ -0,0 +1,21 @@
namespace Lantean.QBTMudBlade.Interop
{
public class BoundingClientRect
{
public int Bottom { get; set; }
public int Top { get; set; }
public int Left { get; set; }
public int Right { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public int X { get; set; }
public int Y { get; set; }
}
}

View File

@@ -0,0 +1,12 @@
using Microsoft.JSInterop;
namespace Lantean.QBTMudBlade.Interop
{
public static class InteropHelper
{
public static async Task<BoundingClientRect?> GetBoundingClientRect(this IJSRuntime runtime, string id)
{
return await runtime.InvokeAsync<BoundingClientRect?>("qbt.getBoundingClientRect", id);
}
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.3" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.3" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="MudBlazor" Version="6.17.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,9 @@
@inherits LayoutComponentBase
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true">
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" />
</MudDrawer>
<MudMainContent>
@Body
</MudMainContent>

View File

@@ -0,0 +1,29 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Layout
{
public partial class DetailsLayout
{
[CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; }
[CascadingParameter]
public IEnumerable<Torrent>? Torrents { get; set; }
protected string? SelectedTorrent { get; set; }
protected override void OnParametersSet()
{
if (Body?.Target is not RouteView routeView || routeView.RouteData.RouteValues is null)
{
return;
}
if (routeView.RouteData.RouteValues.TryGetValue("hash", out var hash))
{
SelectedTorrent = hash?.ToString();
}
}
}
}

View File

@@ -0,0 +1,11 @@
@inherits LayoutComponentBase
@layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true">
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
</MudDrawer>
<MudMainContent>
<CascadingValue Value="SearchTermChanged" Name="SearchTermChanged">
@Body
</CascadingValue>
</MudMainContent>

View File

@@ -0,0 +1,26 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Layout
{
public partial class ListLayout
{
[CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; }
[CascadingParameter(Name = "StatusChanged")]
public EventCallback<Status> StatusChanged { get; set; }
[CascadingParameter(Name = "CategoryChanged")]
public EventCallback<string> CategoryChanged { get; set; }
[CascadingParameter(Name = "TagChanged")]
public EventCallback<string> TagChanged { get; set; }
[CascadingParameter(Name = "TrackerChanged")]
public EventCallback<string> TrackerChanged { get; set; }
[CascadingParameter(Name = "SearchTermChanged")]
public EventCallback<string> SearchTermChanged { get; set; }
}
}

View File

@@ -0,0 +1,57 @@
@inherits LayoutComponentBase
@layout MainLayout
<PageTitle>qBittorrent @Version Web UI</PageTitle>
@if (!IsAuthenticated)
{
<MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="my-7" Style="width: 100%; height: 30px" />
return;
}
<CascadingValue Value="Torrents">
<CascadingValue Value="MainData">
<CascadingValue Value="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">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>
<MudAppBar Bottom="true" Style="background-color: var(--mud-palette-dark-lighten);">
@if (MainData?.LostConnection == true)
{
<MudText Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="pl-1 pr-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" />
<MudText Class="pl-1 pr-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="pl-1 pr-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" />
<MudIcon Class="pl-1 pr-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" />
<MudIcon Class="pl-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowUp" Color="Color.Success" />
<MudText Class="pr-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")")
</MudText>
<MudDivider Vertical="true" />
<MudIcon Class="pl-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Info" />
<MudText Class="pr-1">
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s")
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
</MudText>
</MudAppBar>
</CascadingValue>
</CascadingValue>

View File

@@ -0,0 +1,173 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Layout
{
public partial class LoggedInLayout : IDisposable
{
private readonly bool _refreshEnabled = true;
private int _requestId = 0;
private bool _disposedValue;
private readonly CancellationTokenSource _timerCancellationToken = new();
private int _refreshInterval = 1500;
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; }
protected MainData? MainData { get; set; }
protected string Category { get; set; } = FilterHelper.CATEGORY_ALL;
protected string Tag { get; set; } = FilterHelper.TAG_ALL;
protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL;
protected Status Status { get; set; } = Status.All;
protected string Version { get; set; } = "";
protected string? SearchText { get; set; }
protected IEnumerable<Torrent> Torrents => GetTorrents();
protected bool IsAuthenticated { get; set; }
protected bool LostConnection { get; set; }
private List<Torrent> GetTorrents()
{
if (MainData is null)
{
return [];
}
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
return MainData.Torrents.Values.Filter(filterState).ToList();
}
protected override async Task OnInitializedAsync()
{
if (!await ApiClient.CheckAuthState())
{
NavigationManager.NavigateTo("/login");
return;
}
await InvokeAsync(StateHasChanged);
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
MainData = DataManager.CreateMainData(data);
_requestId = data.ResponseId;
_refreshInterval = MainData.ServerState.RefreshInterval;
IsAuthenticated = true;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_refreshEnabled)
{
return;
}
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_refreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
if (!IsAuthenticated)
{
return;
}
QBitTorrentClient.Models.MainData data;
try
{
data = await ApiClient.GetMainData(_requestId);
}
catch (HttpRequestException)
{
if (MainData is not null)
{
MainData.LostConnection = true;
}
_timerCancellationToken.CancelIfNotDisposed();
await InvokeAsync(StateHasChanged);
return;
}
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data);
}
else
{
DataManager.MergeMainData(data, MainData);
}
_refreshInterval = MainData.ServerState.RefreshInterval;
_requestId = data.ResponseId;
await InvokeAsync(StateHasChanged);
}
}
}
}
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
protected static (string, Color) GetConnectionIcon(string? status)
{
if (status is null)
{
return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
}
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,22 @@
@inherits LayoutComponentBase
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</MudLayout>

View File

@@ -0,0 +1,89 @@
using Lantean.QBitTorrentClient;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using MudBlazor.Services;
namespace Lantean.QBTMudBlade.Layout
{
public partial class MainLayout : IBrowserViewportObserver, IAsyncDisposable
{
private bool _disposedValue;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Inject]
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
[Inject]
private IApiClient ApiClient { get; set; } = default!;
protected bool DrawerOpen { get; set; } = true;
protected bool ShowMenu { get; set; } = false;
public Guid Id => Guid.NewGuid();
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
{
ReportRate = 50,
NotifyOnBreakpointOnly = true
};
protected void ToggleDrawer()
{
DrawerOpen = !DrawerOpen;
}
protected override async Task OnParametersSetAsync()
{
if (!ShowMenu)
{
ShowMenu = await ApiClient.CheckAuthState();
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (firstRender)
{
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
}
await base.OnAfterRenderAsync(firstRender);
}
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
{
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
{
DrawerOpen = false;
}
else if (browserViewportEventArgs.Breakpoint > Breakpoint.Sm && !DrawerOpen)
{
DrawerOpen = true;
}
await InvokeAsync(StateHasChanged);
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
await BrowserViewportService.UnsubscribeAsync(this);
}
_disposedValue = true;
}
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -0,0 +1,14 @@
using Microsoft.AspNetCore.Components.Forms;
namespace Lantean.QBTMudBlade.Models
{
public record AddTorrentFileOptions : TorrentOptions
{
public AddTorrentFileOptions(IReadOnlyList<IBrowserFile> files, TorrentOptions options) : base(options)
{
Files = files;
}
public IReadOnlyList<IBrowserFile> Files { get; }
}
}

View File

@@ -0,0 +1,12 @@
namespace Lantean.QBTMudBlade.Models
{
public record AddTorrentLinkOptions : TorrentOptions
{
public AddTorrentLinkOptions(string urls, TorrentOptions options) : base(options)
{
Urls = urls.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
}
public IReadOnlyList<string> Urls { get; }
}
}

View File

@@ -0,0 +1,14 @@
namespace Lantean.QBTMudBlade.Models
{
public record Category
{
public Category(string name, string savePath)
{
Name = name;
SavePath = savePath;
}
public string Name { get; set; }
public string SavePath { get; set; }
}
}

View File

@@ -0,0 +1,62 @@
namespace Lantean.QBTMudBlade.Models
{
public class ContentItem
{
public ContentItem(
string name,
string displayName,
int index,
Priority priority,
float progress,
long size,
float availability,
bool isFolder = false,
int level = 0)
{
Name = name;
DisplayName = displayName;
Index = index;
Priority = priority;
Progress = progress;
Size = size;
Availability = availability;
IsFolder = isFolder;
Level = level;
}
public string Name { get; }
public string Path => IsFolder ? Name : Name.GetDirectoryPath();
public string DisplayName { get; }
public int Index { get; }
public Priority Priority { get; set; }
public float Progress { get; set; }
public long Size { get; set; }
public float Availability { get; set; }
public long Downloaded => (long)Math.Round(Size * Progress, 0);
public long Remaining => Size - Downloaded;
public bool IsFolder { get; }
public int Level { get; }
public override bool Equals(object? obj)
{
if (obj is null) return false;
return ((ContentItem)obj).Name == Name;
}
public override int GetHashCode()
{
return Name.GetHashCode();
}
}
}

View File

@@ -0,0 +1,8 @@
namespace Lantean.QBTMudBlade.Models
{
public enum ContentItemType
{
File,
Folder
}
}

View File

@@ -0,0 +1,46 @@
namespace Lantean.QBTMudBlade.Models
{
public record GlobalTransferInfo
{
public GlobalTransferInfo(
string connectionStatus,
int dHTNodes,
long downloadInfoData,
long downloadInfoSpeed,
long downloadRateLimit,
long uploadInfoData,
long uploadInfoSpeed,
long uploadRateLimit)
{
ConnectionStatus = connectionStatus;
DHTNodes = dHTNodes;
DownloadInfoData = downloadInfoData;
DownloadInfoSpeed = downloadInfoSpeed;
DownloadRateLimit = downloadRateLimit;
UploadInfoData = uploadInfoData;
UploadInfoSpeed = uploadInfoSpeed;
UploadRateLimit = uploadRateLimit;
}
public GlobalTransferInfo()
{
ConnectionStatus = "Unknown";
}
public string ConnectionStatus { get; set; }
public int DHTNodes { get; set; }
public long DownloadInfoData { get; set; }
public long DownloadInfoSpeed { get; set; }
public long DownloadRateLimit { get; set; }
public long UploadInfoData { get; set; }
public long UploadInfoSpeed { get; set; }
public long UploadRateLimit { get; set; }
}
}

View File

@@ -0,0 +1,40 @@
namespace Lantean.QBTMudBlade.Models
{
public record MainData
{
public MainData(
IDictionary<string, Torrent> torrents,
IEnumerable<string> tags,
IDictionary<string, Category> categories,
IDictionary<string, IReadOnlyList<string>> trackers,
ServerState serverState,
Dictionary<string, HashSet<string>> tagState,
Dictionary<string, HashSet<string>> categoriesState,
Dictionary<string, HashSet<string>> statusState,
Dictionary<string, HashSet<string>> trackersState)
{
Torrents = torrents.ToDictionary();
Tags = tags.ToHashSet();
Categories = categories.ToDictionary();
Trackers = trackers.ToDictionary();
ServerState = serverState;
TagState = tagState;
CategoriesState = categoriesState;
StatusState = statusState;
TrackersState = trackersState;
}
public Dictionary<string, Torrent> Torrents { get; }
public HashSet<string> Tags { get; }
public Dictionary<string, Category> Categories { get; }
public Dictionary<string, IReadOnlyList<string>> Trackers { get; }
public ServerState ServerState { get; }
public Dictionary<string, HashSet<string>> TagState { get; }
public Dictionary<string, HashSet<string>> CategoriesState { get; }
public Dictionary<string, HashSet<string>> StatusState { get; }
public Dictionary<string, HashSet<string>> TrackersState { get; }
public string? SelectedTorrentHash { get; set; }
public bool LostConnection { get; set; }
}
}

View File

@@ -0,0 +1,72 @@
namespace Lantean.QBTMudBlade.Models
{
public class Peer
{
public Peer(
string ip,
string client,
string clientId,
string connection,
string country,
string countryCode,
long downloaded,
long downloadSpeed,
string files,
string flags,
string flagsDescription,
string iPAddress,
int port,
float progress,
float relevance,
long uploaded,
long uploadSpeed)
{
IP = ip;
Client = client;
ClientId = clientId;
Connection = connection;
Country = country;
CountryCode = countryCode;
Downloaded = downloaded;
DownloadSpeed = downloadSpeed;
Files = files;
Flags = flags;
FlagsDescription = flagsDescription;
IPAddress = iPAddress;
Port = port;
Progress = progress;
Relevance = relevance;
Uploaded = uploaded;
UploadSpeed = uploadSpeed;
}
public string IP { get; }
public string Client { get; set; }
public string ClientId { get; set; }
public string Connection { get; set; }
public string Country { get; set; }
public string CountryCode { get; set; }
public long Downloaded { get; set; }
public long DownloadSpeed { get; set; }
public string Files { get; set; }
public string Flags { get; set; }
public string FlagsDescription { get; set; }
public string IPAddress { get; set; }
public int Port { get; set; }
public float Progress { get; set; }
public float Relevance { get; set; }
public long Uploaded { get; set; }
public long UploadSpeed { get; set; }
public override bool Equals(object? obj)
{
if (obj is null) return false;
return ((Peer)obj).IP == IP;
}
public override int GetHashCode()
{
return IP.GetHashCode();
}
}
}

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