36 Commits

Author SHA1 Message Date
ahjephson
9d8d84168e Merge pull request #7 from lantean-code/codex/find-and-fix-a-bug
Fix typo in SaveLocation property
2025-10-20 10:15:48 +01:00
ahjephson
bb66b97f45 Merge branch 'develop' into codex/find-and-fix-a-bug 2025-10-20 10:14:47 +01:00
ahjephson
d4ac79af00 Merge pull request #10 from lantean-code/feature/bugfixes
- Fixed an issue where the tag wasn't being correctly applied to the filter in qBittorrent 5.1+ (#9)
- Fixed an issue where the category wasn't being applied to the filter correctly (#9)
- Fixed invalid ValueChanged for "Default Torrent Management Mode"
- Fixed a crash where TimeSpan.FromSeconds was crashing
- Fixed an invalid icon to appear when Paused/Stopped
2025-10-18 16:18:10 +01:00
ahjephson
7370d73c59 Fix minor display issues 2025-10-18 16:01:53 +01:00
ahjephson
8796cc0f24 Fix #9 and bug related to invalid TimeSpan in duration 2025-10-18 15:37:04 +01:00
ahjephson
b24ae440d4 Merge pull request #8 from ehaughee/develop
Fix MaxRatio to allow float values
2025-10-02 15:01:10 +01:00
Eric Haughee
bb90ce5216 Fix MaxRatio to allow float values 2025-09-20 18:29:18 -07:00
ahjephson
4eaa46b2b3 Fix property name in SaveLocation 2025-06-03 09:02:47 +01:00
ahjephson
1cf9f97187 Merge tag '1.1.0' into develop
1.1.0
2025-05-30 15:46:03 +01:00
ahjephson
4f9129fd46 Merge branch 'release/1.1.0' 2025-05-30 15:45:32 +01:00
ahjephson
9a9d2c2ee2 Update packages 2025-05-30 15:43:22 +01:00
ahjephson
736bc46745 Merge pull request #2 from lantean-code/feature/fix-statuses
Fix Paused/Stopped Duplicate
2025-05-30 14:19:34 +01:00
ahjephson
23ae19c4c7 Update readme.md 2025-05-20 13:35:59 +01:00
ahjephson
603470eb30 Merge pull request #3 from lantean-code/feature/fix-relative-resources
Fix Reverse Proxy Issue
2025-05-20 13:24:53 +01:00
ahjephson
27c2406340 Fixes #1 2025-04-22 14:08:55 +01:00
ahjephson
4578dcc11f FIx issue with duplicate paused/stopped status lists when handling v4/5 differences 2025-04-22 14:03:33 +01:00
ahjephson
3215fa3936 Merge tag '1.0.2' into develop
1.0.2
2025-03-22 13:52:41 +00:00
ahjephson
78e62f31d0 Merge branch 'hotfix/1.0.2' 2025-03-22 13:52:33 +00:00
ahjephson
e23842fcb0 Fix invalid exception being caught. 2025-03-22 13:51:44 +00:00
ahjephson
411c7f87cc Merge tag '1.0.1' into develop
1.0.1
2025-02-10 08:57:20 +00:00
ahjephson
4098f8f5a9 Merge branch 'hotfix/1.0.1' 2025-02-10 08:57:01 +00:00
ahjephson
12f81c5978 Fix issue with TorrentActions treating actions as all downloaded. 2025-02-10 08:55:46 +00:00
ahjephson
717738d720 Update readme.md 2025-02-07 13:24:25 +00:00
ahjephson
885c34c8cf Update readme.md 2025-02-07 13:10:15 +00:00
ahjephson
ef3c68a6aa Merge tag '1.0.0' into develop
1.0.0
2025-02-07 13:02:16 +00:00
ahjephson
a29e64fc1b Merge branch 'release/1.0.0' 2025-02-07 13:01:36 +00:00
ahjephson
e55955c75e Fix small screen issues 2025-02-07 11:49:37 +00:00
ahjephson
aa80396862 Update dotnet.yml 2025-02-07 09:53:01 +00:00
ahjephson
30ced3293c Update dotnet.yml 2025-02-07 09:52:18 +00:00
ahjephson
c54f73a517 Merge tag '0.2.0' into develop
0.2.0
2025-02-07 09:49:18 +00:00
ahjephson
bad509e40f Merge branch 'release/0.2.0' 2025-02-07 09:48:56 +00:00
ahjephson
6a0796ef20 Update actions to build .net 9 2025-02-07 09:46:14 +00:00
ahjephson
dc4b515763 Fix styling issues with torrent list
Only display errors in debug mode
Add column sorting
2025-02-07 09:23:54 +00:00
ahjephson
938702a7b3 Partial .net9 upgrade 2025-02-04 13:58:24 +00:00
ahjephson
6ca1c6edd4 Update to net9.0 2025-01-07 09:18:45 +00:00
ahjephson
24eb5cf5e9 Merge tag '0.1.0' into develop
0.1.0
2024-11-02 13:46:03 +00:00
68 changed files with 607 additions and 329 deletions

View File

@@ -21,12 +21,12 @@ jobs:
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '8.0.x'
dotnet-version: '9.0.x'
- name: Install GitVersion
uses: gittools/actions/gitversion/setup@v3.0.0
with:
versionSpec: '6.x'
versionSpec: '6.0.0'
- name: Determine Version
id: gitversion

View File

@@ -1,24 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.9.2" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Test
Test2(a => a.Name);
}
private void Test2(Expression<Func<TestClass, object>> expr)
private void Test2(Expression<Func<TestClass, object?>> expr)
{
var body = expr.Body;
}
@@ -38,7 +38,7 @@ namespace Lantean.QBTMud.Test
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
Expression<Func<TestClass, object>> expr2 = a => a.Name;
Expression<Func<TestClass, object?>> expr2 = a => a.Name;
var x = l.Compile();
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
@@ -58,9 +58,9 @@ namespace Lantean.QBTMud.Test
public class TestClass
{
public string Name { get; set; }
public string? Name { get; set; }
public string Description { get; set; }
public string? Description { get; set; }
public long Value { get; set; }
}

View File

@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class AddPeerDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
public IMudDialogInstance MudDialog { get; set; } = default!;
protected HashSet<PeerId> Peers { get; } = [];

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
<MudGrid>
<MudItem xs="12">
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPosition="LabelPosition.End" />
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
</MudItem>
</MudGrid>
<MudCollapse Expanded="Expanded">

View File

@@ -1,6 +1,7 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMud.Components.Dialogs
{

View File

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

View File

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

View File

@@ -5,12 +5,13 @@
<DialogContent>
<MudCard Class="w-100" Elevation="0">
<MudGrid>
@for (var i = 0; i < Columns.Count; i++)
@for (var i = 0; i < OrderedColumns.Length; i++)
{
var column = Columns[i];
var item = OrderedColumns[i];
var column = Columns.First(c => c.Id == item);
var index = i;
<MudItem xs="7">
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPlacement="Placement.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
</MudItem>
<MudItem xs="3">
<MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />

View File

@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
public partial class ColumnOptionsDialog<T>
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
private IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
[EditorRequired]
@@ -20,10 +20,15 @@ namespace Lantean.QBTMud.Components.Dialogs
[Parameter]
public Dictionary<string, int?> Widths { get; set; } = [];
[Parameter]
public Dictionary<string, int> Order { get; set; } = [];
protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
protected Dictionary<string, int> OrderInternal { get; set; } = [];
protected override void OnParametersSet()
{
if (SelectedColumnsInternal.Count == 0)
@@ -51,6 +56,25 @@ namespace Lantean.QBTMud.Components.Dialogs
WidthsInternal[width.Key] = width.Value;
}
}
if (OrderInternal.Count == 0)
{
if (Order.Count == 0)
{
for (int i = 0; i < Columns.Count; i++)
{
var column = Columns[i];
OrderInternal.Add(column.Id, i);
}
}
else
{
foreach (var order in Order)
{
OrderInternal[order.Key] = order.Value;
}
}
}
}
protected void SetSelected(bool selected, string id)
@@ -101,7 +125,15 @@ namespace Lantean.QBTMud.Components.Dialogs
return;
}
(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index - 1).Key;
OrderInternal[otherId] = index;
OrderInternal[currentId] = index - 1;
//(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
StateHasChanged();
}
protected void MoveDown(int index)
@@ -111,7 +143,15 @@ namespace Lantean.QBTMud.Components.Dialogs
return;
}
(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index + 1).Key;
OrderInternal[otherId] = index;
OrderInternal[currentId] = index + 1;
//(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
StateHasChanged();
}
protected string GetValue(int? value, string columnId)
@@ -134,6 +174,13 @@ namespace Lantean.QBTMud.Components.Dialogs
return value.Value.ToString();
}
private string[] OrderedColumns => GetOrderedColumns();
private string[] GetOrderedColumns()
{
return OrderInternal.OrderBy(x => x.Value).Select(x => x.Key).ToArray();
}
protected void Cancel()
{
MudDialog.Cancel();
@@ -141,7 +188,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected void Submit()
{
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal)));
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal, OrderInternal)));
}
protected override Task Submit(KeyboardEvent keyboardEvent)

View File

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

View File

@@ -6,7 +6,7 @@
<MudGrid>
<MudItem xs="12">
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPosition="LabelPosition.End" />
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPlacement="Placement.End" />
</MudItem>
</MudGrid>
</DialogContent>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -6,7 +6,6 @@ using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components;
using MudBlazor;
using System.Collections.ObjectModel;
using static MudBlazor.Colors;
namespace Lantean.QBTMud.Components.Dialogs
{
@@ -31,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected ILocalStorageService LocalStorage { get; set; } = default!;
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Hash { get; set; }

View File

@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
private readonly List<string> _unsavedRuleNames = [];
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
IMudDialogInstance MudDialog { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
</ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
<MudDivider Vertical="true" />
@@ -22,6 +23,7 @@
<MudSpacer />
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
</div>
<DynamicTable
@ref="Table"

View File

@@ -1,4 +1,4 @@
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
@TorrentControls(_statusType)
</ContextMenu>

View File

@@ -92,7 +92,9 @@
<FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
</MudItem>
<MudItem xs="9">
<MudNumericField T="int" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged" Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined" Validation="MaxRatioValidation" />
<MudNumericField T="float" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged"
Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined"
Validation="MaxRatioValidation" />
</MudItem>
<MudItem xs="3">
<FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />

View File

@@ -17,7 +17,7 @@
protected int SlowTorrentUlRateThreshold { get; private set; }
protected int SlowTorrentInactiveTimer { get; private set; }
protected bool MaxRatioEnabled { get; private set; }
protected int MaxRatio { get; private set; }
protected float MaxRatio { get; private set; }
protected bool MaxSeedingTimeEnabled { get; private set; }
protected int MaxSeedingTime { get; private set; }
protected int MaxRatioAct { get; private set; }
@@ -275,7 +275,7 @@
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task MaxRatioChanged(int value)
protected async Task MaxRatioChanged(float value)
{
MaxRatio = value;
UpdatePreferences.MaxRatio = value;

View File

@@ -62,7 +62,7 @@
<MudCardContent Class="pt-0">
<MudGrid>
<MudItem xs="12">
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" Variant="Variant.Outlined">
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoTmmEnabledChanged" Variant="Variant.Outlined">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>

View File

@@ -191,13 +191,13 @@ else if (RenderType == RenderType.MenuItems)
if (!action.Children.Any())
{
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled" Class="icon-menu-dense">
@action.Text
</MudMenuItem>
}
else
{
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))">
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))" Class="icon-menu-dense">
<MudMenu ListClass="unselectable" Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" Ripple="false" Class="sub-menu">
<ActivatorContent>
@action.Text

View File

@@ -12,10 +12,7 @@ namespace Lantean.QBTMud.Components
{
public partial class TorrentActions : IAsyncDisposable
{
private const int _defaultVersion = 5;
private bool _disposedValue;
private int? _version;
private List<UIAction>? _actions;
@@ -63,7 +60,7 @@ namespace Lantean.QBTMud.Components
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[Parameter]
public MudDialogInstance? MudDialog { get; set; }
public IMudDialogInstance? MudDialog { get; set; }
[Parameter]
public UIAction? ParentAction { get; set; }
@@ -74,37 +71,14 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; }
protected int MajorVersion
{
get
{
if (_version is not null)
{
return _version.Value;
}
if (string.IsNullOrEmpty(Version))
{
return _defaultVersion;
}
if (!System.Version.TryParse(Version.Replace("v", ""), out var version))
{
return _defaultVersion;
}
_version = version.Major;
return _version.Value;
}
}
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
protected override void OnInitialized()
{
_actions =
[
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
@@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components
thereAreFirstLastPiecePrio = true;
}
if (torrent.Progress != 1.0) // not downloaded
if (torrent.Progress < 0.999999) // not downloaded
{
allAreDownloaded = false;
}

View File

@@ -8,19 +8,17 @@
Class="unselectable"
MaxHeight="@MaxHeight"
AnchorOrigin="@AnchorOrigin"
TransformOrigin="TransformOrigin"
RelativeWidth="@FullWidth"
TransformOrigin="@TransformOrigin"
RelativeWidth="@RelativeWidth"
OverflowBehavior="OverflowBehavior.FlipAlways"
Style="@_popoverStyle"
@ontouchend:preventDefault>
<CascadingValue Value="@(FakeMenu)">
@if (_showChildren)
{
<MudList T="object"
Class="unselectable"
Dense="@Dense">
<MudList T="object" Class="unselectable" Dense="@Dense">
@ChildContent
</MudList>
</MudList>
}
</CascadingValue>
</MudPopover>

View File

@@ -7,12 +7,6 @@ using MudBlazor.Utilities;
namespace Lantean.QBTMud.Components.UI
{
// This is a very hacky approach but works for now.
// This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked.
// MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't.
// Ideally this should be changed to use something like the way the DialogService works.
// Or - rework this to have a hidden MudMenu and hook into the OpenChanged event to monitor when the MudMenuItem closes it.
public partial class ContextMenu : MudComponentBase
{
private bool _open;
@@ -61,7 +55,7 @@ namespace Lantean.QBTMud.Components.UI
/// </summary>
[Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)]
public bool FullWidth { get; set; }
public DropdownWidth RelativeWidth { get; set; }
/// <summary>
/// Sets the max height the menu can have when open.
@@ -219,56 +213,58 @@ namespace Lantean.QBTMud.Components.UI
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
protected override Task OnAfterRenderAsync(bool firstRender)
{
if (!_isResized)
{
await DeterminePosition();
//await DeterminePosition();
}
return Task.CompletedTask;
}
private async Task DeterminePosition()
{
var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
double? contextMenuHeight = null;
double? contextMenuWidth = null;
//private async Task DeterminePosition()
//{
// var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
// double? contextMenuHeight = null;
// double? contextMenuWidth = null;
var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
// var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
if (popoverSize.Height > 0)
{
contextMenuHeight = popoverSize.Height;
contextMenuWidth = popoverSize.Width;
}
else
{
return;
}
// var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
// if (popoverSize.Height > 0)
// {
// contextMenuHeight = popoverSize.Height;
// contextMenuWidth = popoverSize.Width;
// }
// else
// {
// return;
// }
// the bottom position of the popover will be rendered off screen
if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
{
// adjust the top of the context menu
var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
_y -= overshoot;
// // the bottom position of the popover will be rendered off screen
// if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
// {
// // adjust the top of the context menu
// var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
// _y -= overshoot;
if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
{
MaxHeight = (int)(mainContentSize.Height - _y + _diff);
}
}
// if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
// {
// MaxHeight = (int)(mainContentSize.Height - _y + _diff);
// }
// }
if (_x + contextMenuWidth.Value > mainContentSize.Width)
{
var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
_x -= overshoot;
}
// if (_x + contextMenuWidth.Value > mainContentSize.Width)
// {
// var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
// _x -= overshoot;
// }
SetPopoverStyle(_x, _y);
_isResized = true;
await InvokeAsync(StateHasChanged);
}
// SetPopoverStyle(_x, _y);
// _isResized = true;
// await InvokeAsync(StateHasChanged);
//}
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
{

View File

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

View File

@@ -13,6 +13,7 @@ namespace Lantean.QBTMud.Components.UI
private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
private readonly string _columnOrderStorageKey = $"DynamicTable{_typeName}.ColumnOrder";
[Inject]
public ILocalStorageService LocalStorage { get; set; } = default!;
@@ -82,6 +83,8 @@ namespace Lantean.QBTMud.Components.UI
private Dictionary<string, int?> _columnWidths = [];
private Dictionary<string, int> _columnOrder = [];
private string? _sortColumn;
private SortDirection _sortDirection;
@@ -165,8 +168,29 @@ namespace Lantean.QBTMud.Components.UI
protected IEnumerable<ColumnDefinition<T>> GetColumns()
{
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
foreach (var column in filteredColumns)
if (_columnOrder.Count == 0)
{
foreach (var column in filteredColumns)
{
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
}
yield return column;
}
yield break;
}
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key))
{
if (!columnDictionary.TryGetValue(columnId, out var column))
{
continue;
}
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
@@ -280,7 +304,7 @@ namespace Lantean.QBTMud.Components.UI
public async Task ShowColumnOptionsDialog()
{
var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths);
var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths, _columnOrder);
if (result == default)
{
@@ -299,11 +323,17 @@ namespace Lantean.QBTMud.Components.UI
_columnWidths = result.ColumnWidths;
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
}
if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
{
_columnOrder = result.ColumnOrder;
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
}
}
private static bool DictionaryEqual(Dictionary<string, int?> left, Dictionary<string, int?> right)
private static bool DictionaryEqual<TKey, TValue>(Dictionary<TKey, TValue> left, Dictionary<TKey, TValue> right) where TKey : notnull
{
return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && left[k] == right[k]);
return left.Keys.Count == right.Keys.Count && left.Keys.All(k => right.ContainsKey(k) && Equals(left[k], right[k]));
}
private static string? GetColumnStyle(ColumnDefinition<T> column)

View File

@@ -328,13 +328,14 @@ namespace Lantean.QBTMud.Helpers
return tags;
}
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths)
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths, Dictionary<string, int> ColumnOrder)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths, Dictionary<string, int> order)
{
var parameters = new DialogParameters
{
{ nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
{ nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
{ nameof(ColumnOptionsDialog<T>.Widths), widths },
{ nameof(ColumnOptionsDialog<T>.Order), order },
};
var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
@@ -344,7 +345,7 @@ namespace Lantean.QBTMud.Helpers
return default;
}
return ((HashSet<string>, Dictionary<string, int?>))dialogResult.Data;
return ((HashSet<string>, Dictionary<string, int?>, Dictionary<string, int>))dialogResult.Data;
}
public static async Task<bool> ShowConfirmDialog(this IDialogService dialogService, string title, string content)

View File

@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
{
if (seconds is null)
{
return "";
return string.Empty;
}
if (seconds == 8640000)
const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
var value = seconds.Value;
if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
{
return "∞";
}
if (seconds < 60)
if (value <= 0)
{
return "< 1m";
}
TimeSpan time;
try
var time = TimeSpan.FromSeconds(value);
if (time.TotalMinutes < 1)
{
time = TimeSpan.FromSeconds(seconds.Value);
}
catch (OverflowException)
{
return "∞";
return "< 1m";
}
var sb = new StringBuilder();
if (prefix is not null)
{
@@ -83,6 +83,7 @@ namespace Lantean.QBTMud.Helpers
return sb.ToString();
}
/// <summary>
/// Formats a file size in bytes into an appropriate unit based on the size.
/// </summary>
@@ -129,7 +130,7 @@ namespace Lantean.QBTMud.Helpers
return "";
}
return Size(size);
return Size(size, prefix, suffix);
}
/// <summary>

View File

@@ -119,34 +119,34 @@ namespace Lantean.QBTMud.Helpers
switch (category)
{
case CATEGORY_ALL:
break;
return true;
case CATEGORY_UNCATEGORIZED:
if (!string.IsNullOrEmpty(torrent.Category))
{
return false;
}
break;
return true;
default:
if (string.IsNullOrEmpty(torrent.Category))
{
return false;
}
if (!useSubcategories)
{
if (torrent.Category != category)
{
return false;
}
else
{
if (!torrent.Category.StartsWith(category))
{
return false;
}
}
return string.Equals(torrent.Category, category, StringComparison.Ordinal);
}
break;
}
return true;
if (string.Equals(torrent.Category, category, StringComparison.Ordinal))
{
return true;
}
var prefix = string.Concat(category, "/");
return torrent.Category.StartsWith(prefix, StringComparison.Ordinal);
}
}
public static bool FilterTag(Torrent torrent, string tag)
@@ -207,7 +207,7 @@ namespace Lantean.QBTMud.Helpers
break;
case Status.Paused:
if (!state.Contains("paused") || !state.Contains("stopped"))
if (!state.Contains("paused") && !state.Contains("stopped"))
{
return false;
}

View File

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

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

View File

@@ -1,24 +1,22 @@
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CompressionEnabled>false</CompressionEnabled>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CompressionEnabled>false</CompressionEnabled>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
<PackageReference Include="MudBlazor" Version="7.15.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="2.1.0" />
<!-- added to fix vuln in dependency -->
<PackageReference Include="System.Text.Json" Version="8.0.5" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
<PackageReference Include="MudBlazor" Version="8.7.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -36,23 +36,23 @@
</CascadingValue>
</CascadingValue>
</CascadingValue>
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten);">
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
@if (MainData?.LostConnection == true)
{
<MudText Class="mx-2 mb-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" />
<MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")

View File

@@ -83,7 +83,7 @@ namespace Lantean.QBTMud.Layout
Preferences = await ApiClient.GetApplicationPreferences();
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
MainData = DataManager.CreateMainData(data);
MainData = DataManager.CreateMainData(data, Version);
_requestId = data.ResponseId;
_refreshInterval = MainData.ServerState.RefreshInterval;
@@ -128,7 +128,7 @@ namespace Lantean.QBTMud.Layout
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data);
MainData = DataManager.CreateMainData(data, Version);
}
else
{

View File

@@ -20,16 +20,18 @@
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge>
}
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
<MudSwitch T="bool" Label="Dark Mode" LabelPlacement="Placement.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
<Menu @ref="Menu" />
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<MudDrawer @bind-Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="Theme">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
<CascadingValue Value="Menu">
@Body
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
private bool _disposedValue;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Inject]
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
@@ -78,21 +75,21 @@ namespace Lantean.QBTMud.Layout
{
IsDarkMode = isDarkMode.Value;
}
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
await InvokeAsync(StateHasChanged);
}
}
protected Task OnSystemPreferenceChanged(bool value)
protected Task OnSystemDarkModeChanged(bool value)
{
IsDarkMode = value;
return Task.CompletedTask;
}
public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
{
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
if (browserViewportEventArgs.Breakpoint <= Breakpoint.Sm)
{
DrawerOpen = false;
}
@@ -101,7 +98,17 @@ namespace Lantean.QBTMud.Layout
DrawerOpen = true;
}
return Task.CompletedTask;
if (ErrorBoundary?.Errors.Count > 0)
{
ErrorDrawerOpen = true;
}
else
{
await Task.Delay(250);
ErrorDrawerOpen = false;
}
await InvokeAsync(StateHasChanged);
}
protected void ToggleErrorDrawer()

View File

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

View File

@@ -8,6 +8,7 @@
Completed,
Resumed,
Paused,
Stopped,
Active,
Inactive,
Stalled,
@@ -15,6 +16,6 @@
StalledDownloading,
Checking,
Errored,
Stopped
}
}

View File

@@ -12,89 +12,96 @@
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
<MudTabPanel Text="About">
<div class="d-flex gap-4">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop" Height="162" Width="94" />
<MudGrid Class="mx-0 mt-0 mb-3">
<MudItem xs="12">
<div class="d-flex gap-3">
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None" Alt="QBT" Height="32" Width="32" /><MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudGrid Class="mt-0 mb-4">
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop"
Height="162" Width="94" />
</MudItem>
<MudItem xs="12" sm="9" md="10" lg="10" xl="11">
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-3 align-items-center">
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None"
Alt="QBT" Height="32" Width="32" />
<MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
</div>
<MudText Typo="Typo.body1">
An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.
</MudText>
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Home Page: </MudText>
<MudLink Href="https://www.qbittorrent.org" Target="_blank" Class="ms-2">
qbittorrent.org
</MudLink>
</div>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Bug Tracker: </MudText>
<MudLink Href="https://bugs.qbittorrent.org" Target="_blank" Class="ms-2">
bugs.qbittorrent.org
</MudLink>
</div>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Forum: </MudText>
<MudLink Href="https://forum.qbittorrent.org" Target="_blank" Class="ms-2">
forum.qbittorrent.org
</MudLink>
</div>
</div>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body1">An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Home Page</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://www.qbittorrent.org" Target="https://www.qbittorrent.org">https://www.qbittorrent.org</MudLink>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Bug Tracker</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://bugs.qbittorrent.org" Target="https://bugs.qbittorrent.org">https://bugs.qbittorrent.org</MudLink>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Forum</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://forum.qbittorrent.org" Target="https://forum.qbittorrent.org">https://forum.qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
</div>
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Authors">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudText Typo="Typo.body1" Class="py-1">Current maintainer</MudText>
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
<MudGrid Class="mt-0 mb-4">
<MudItem xs="2">
<MudText Typo="Typo.body1">Name</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Name</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Sledgehammer999</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Nationality</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Nationality</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Greece</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">E-mail</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">E-mail</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
<MudText Typo="Typo.body1" Class="py-1">Original author</MudText>
<MudText Typo="Typo.h5" Class="py-1">Original author</MudText>
<MudGrid Class="mt-0 mb-4">
<MudItem xs="2">
<MudText Typo="Typo.body1">Name</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Name</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Christophe Dumez</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Nationality</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Nationality</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">France</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">E-mail</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">E-mail</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
@@ -118,7 +125,7 @@
(the list might not be up to date)
</MudText>
<MudList T="string" ReadOnly>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Info"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Armenian:</u> Hrant Ohanyan (hrantohanyan@mail.am)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@gmail.com)</MudListItem>
@@ -1058,38 +1065,38 @@
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
<MudGrid Class="mt-1 mb-4">
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Qt</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@QtVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Libtorrent</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Boost</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@BoostVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">OpenSSL</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@OpensslVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">zlib</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@ZlibVersion</MudText>
</MudItem>
</MudGrid>

View File

@@ -1,6 +1,7 @@
@page "/details/{hash}"
@layout DetailsLayout
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
@@ -14,6 +15,7 @@
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>
</MudToolBar>
</div>
@if (ShowTabs)
{

View File

@@ -49,7 +49,7 @@ namespace Lantean.QBTMud.Pages
protected override Task OnInitializedAsync()
{
return DoLogin("admin", "eBGJzbjkJ");
return DoLogin("admin", "5FUM5pATq");
}
#endif

View File

@@ -18,7 +18,7 @@
<MudItem xs="12" md="4">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="2" md="3">
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
@foreach (var (value, name) in Categories)
{
@@ -30,17 +30,21 @@
}
</MudSelect>
</MudItem>
<MudItem xs="2" md="3">
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
<MudSelectItem Value="@("all")">All</MudSelectItem>
<MudDivider />
@if (Plugins.Count > 0)
{
<MudDivider />
}
@foreach (var (value, name) in Plugins)
{
<MudSelectItem Value="value">@name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="2" md="2">
<MudItem xs="12" md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
</MudItem>

View File

@@ -1,12 +1,13 @@
@page "/"
@layout ListLayout
<ContextMenu @ref="ContextMenu" Dense="true" AdjustmentX="@(DrawerOpen ? -235 : 0)">
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
<MudDivider />
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
</ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
@@ -18,6 +19,7 @@
<MudSpacer />
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar>
</div>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable

View File

@@ -193,7 +193,7 @@ namespace Lantean.QBTMud.Pages
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
[
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("#", t => t.Priority),
ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25),
ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25, tdClass: "table-icon"),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Name", t => t.Name, width: 400),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),

View File

@@ -5,7 +5,7 @@ namespace Lantean.QBTMud.Services
{
public class DataManager : IDataManager
{
private static readonly Status[] _statuses = Enum.GetValues<Status>();
private static Status[]? _statusArray = null;
public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers)
{
@@ -25,8 +25,9 @@ namespace Lantean.QBTMud.Services
return peerList;
}
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData)
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version)
{
var majorVersion = VersionHelper.GetMajorVersion(version);
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
if (mainData.Torrents is not null)
{
@@ -38,12 +39,19 @@ namespace Lantean.QBTMud.Services
}
}
var tags = new List<string>(mainData.Tags?.Count ?? 0);
var tags = new List<string>();
if (mainData.Tags is not null)
{
var seenTags = new HashSet<string>(StringComparer.Ordinal);
foreach (var tag in mainData.Tags)
{
tags.Add(tag);
var normalizedTag = NormalizeTag(tag);
if (string.IsNullOrEmpty(normalizedTag) || !seenTags.Add(normalizedTag))
{
continue;
}
tags.Add(normalizedTag);
}
}
@@ -87,8 +95,9 @@ namespace Lantean.QBTMud.Services
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
}
var statusState = new Dictionary<string, HashSet<string>>(_statuses.Length + 2);
foreach (var status in _statuses)
var statuses = GetStatuses(majorVersion).ToArray();
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
foreach (var status in statuses)
{
statusState.Add(status.ToString(), torrents.Values.Where(t => FilterHelper.FilterStatus(t, status)).ToHashesHashSet());
}
@@ -101,7 +110,7 @@ namespace Lantean.QBTMud.Services
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet());
}
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState);
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState, majorVersion);
return torrentList;
}
@@ -155,8 +164,14 @@ namespace Lantean.QBTMud.Services
{
foreach (var tag in mainData.TagsRemoved)
{
torrentList.Tags.Remove(tag);
torrentList.TagState.Remove(tag);
var normalizedTag = NormalizeTag(tag);
if (string.IsNullOrEmpty(normalizedTag))
{
continue;
}
torrentList.Tags.Remove(normalizedTag);
torrentList.TagState.Remove(normalizedTag);
}
}
@@ -198,7 +213,18 @@ namespace Lantean.QBTMud.Services
{
foreach (var tag in mainData.Tags)
{
torrentList.Tags.Add(tag);
var normalizedTag = NormalizeTag(tag);
if (string.IsNullOrEmpty(normalizedTag))
{
continue;
}
torrentList.Tags.Add(normalizedTag);
var matchingHashes = torrentList.Torrents
.Where(pair => FilterHelper.FilterTag(pair.Value, normalizedTag))
.Select(pair => pair.Key)
.ToHashSet();
torrentList.TagState[normalizedTag] = matchingHashes;
}
}
@@ -206,7 +232,7 @@ namespace Lantean.QBTMud.Services
{
foreach (var (url, hashes) in mainData.Trackers)
{
if (!torrentList.Trackers.TryGetValue(url, out var existingHashes))
if (!torrentList.Trackers.TryGetValue(url, out _))
{
torrentList.Trackers.Add(url, hashes);
}
@@ -225,7 +251,7 @@ namespace Lantean.QBTMud.Services
{
var newTorrent = CreateTorrent(hash, torrent);
torrentList.Torrents.Add(hash, newTorrent);
AddTorrentToStates(torrentList, hash);
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
}
else
{
@@ -241,7 +267,7 @@ namespace Lantean.QBTMud.Services
}
}
private static void AddTorrentToStates(MainData torrentList, string hash)
private static void AddTorrentToStates(MainData torrentList, string hash, int version)
{
var torrent = torrentList.Torrents[hash];
@@ -271,7 +297,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(version))
{
torrentList.StatusState[status.ToString()].AddIfTrue(hash, FilterHelper.FilterStatus(torrent, status));
}
@@ -289,6 +315,25 @@ namespace Lantean.QBTMud.Services
}
}
private static Status[] GetStatuses(int version)
{
if (_statusArray is not null)
{
return _statusArray;
}
if (version == 5)
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Paused).ToArray();
}
else
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Stopped).ToArray();
}
return _statusArray;
}
private static void UpdateTorrentStates(MainData torrentList, string hash)
{
var torrent = torrentList.Torrents[hash];
@@ -317,7 +362,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(torrentList.MajorVersion))
{
torrentList.StatusState[status.ToString()].AddIfTrueOrRemove(hash, FilterHelper.FilterStatus(torrent, status));
}
@@ -361,7 +406,7 @@ namespace Lantean.QBTMud.Services
categoryState.RemoveIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(torrentList.MajorVersion))
{
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
{
@@ -487,6 +532,12 @@ namespace Lantean.QBTMud.Services
public Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent)
{
var normalizedTags = torrent.Tags?
.Select(NormalizeTag)
.Where(static tag => !string.IsNullOrEmpty(tag))
.ToList()
?? new List<string>();
return new Torrent(
hash,
torrent.AddedOn.GetValueOrDefault(),
@@ -527,7 +578,7 @@ namespace Lantean.QBTMud.Services
torrent.Size.GetValueOrDefault(),
torrent.State!,
torrent.SuperSeeding.GetValueOrDefault(),
torrent.Tags!,
normalizedTags,
torrent.TimeActive.GetValueOrDefault(),
torrent.TotalSize.GetValueOrDefault(),
torrent.Tracker!,
@@ -540,6 +591,19 @@ namespace Lantean.QBTMud.Services
torrent.MaxInactiveSeedingTime.GetValueOrDefault());
}
private static string NormalizeTag(string? tag)
{
if (string.IsNullOrEmpty(tag))
{
return string.Empty;
}
var separatorIndex = tag.IndexOf('\t');
var normalized = (separatorIndex >= 0) ? tag[..separatorIndex] : tag;
return normalized.Trim();
}
private static void UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category)
{
existingCategory.SavePath = category.SavePath ?? existingCategory.SavePath;
@@ -588,7 +652,14 @@ namespace Lantean.QBTMud.Services
if (torrent.Tags is not null)
{
existingTorrent.Tags.Clear();
existingTorrent.Tags.AddRange(torrent.Tags);
foreach (var tag in torrent.Tags)
{
var normalizedTag = NormalizeTag(tag);
if (!string.IsNullOrEmpty(normalizedTag))
{
existingTorrent.Tags.Add(normalizedTag);
}
}
}
existingTorrent.TimeActive = torrent.TimeActive ?? existingTorrent.TimeActive;
existingTorrent.TotalSize = torrent.TotalSize ?? existingTorrent.TotalSize;

View File

@@ -4,7 +4,7 @@ namespace Lantean.QBTMud.Services
{
public interface IDataManager
{
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);

View File

@@ -155,7 +155,7 @@ code {
}
.torrent-list .mud-table-container {
height: calc(100vh - 149px);
height: calc(100vh - 160px);
}
.file-list .mud-table-container {
@@ -240,4 +240,19 @@ td .folder-button {
.mud-dialog .mud-dialog-content {
padding-top: 4px !important;
}
.icon-menu-dense {
padding-top: 2px;
padding-bottom: 2px;
}
.table-icon {
width: 25px;
max-width: 25px;
padding: 0 8px !important;
}
.mud-popover .mud-divider:last-child {
display: none;
}

View File

@@ -9,12 +9,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="css/app.css" />
<link href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="./css/app.css" />
<link rel="icon" type="image/png" href="images/qbittorrent32.png" />
<link rel="icon" href="images/qbittorrent-tray.svg">
<link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000">
<link rel="apple-touch-icon" href="images/qbittorrent32.png">
<link rel="icon" href="./images/qbittorrent-tray.svg">
<link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
<link rel="apple-touch-icon" href="./images/qbittorrent32.png">
</head>
<body>
@@ -31,10 +31,10 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="js/piecesbar.js"></script>
<script src="js/interop.js"></script>
<script src="./_framework/blazor.webassembly.js"></script>
<script src="./_content/MudBlazor/MudBlazor.min.js"></script>
<script src="./js/piecesbar.js"></script>
<script src="./js/interop.js"></script>
</body>
</html>

View File

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

View File

@@ -1,10 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@@ -112,7 +112,7 @@ namespace Lantean.QBitTorrentClient.Models
int maxConnecPerTorrent,
int maxInactiveSeedingTime,
bool maxInactiveSeedingTimeEnabled,
int maxRatio,
float maxRatio,
int maxRatioAct,
bool maxRatioEnabled,
int maxSeedingTime,
@@ -745,7 +745,7 @@ namespace Lantean.QBitTorrentClient.Models
public bool MaxInactiveSeedingTimeEnabled { get; }
[JsonPropertyName("max_ratio")]
public int MaxRatio { get; }
public float MaxRatio { get; }
[JsonPropertyName("max_ratio_act")]
public int MaxRatioAct { get; }

View File

@@ -4,7 +4,7 @@
{
public bool IsWatchedFolder { get; set; }
public bool IsDefaltFolder { get; set; }
public bool IsDefaultFolder { get; set; }
public string? SavePath { get; set; }
@@ -23,7 +23,7 @@
{
return new SaveLocation
{
IsDefaltFolder = true
IsDefaultFolder = true
};
}
}
@@ -40,7 +40,7 @@
{
return new SaveLocation
{
IsDefaltFolder = true
IsDefaultFolder = true
};
}
else
@@ -61,7 +61,7 @@
{
return 0;
}
else if (IsDefaltFolder)
else if (IsDefaultFolder)
{
return 1;
}

View File

@@ -323,7 +323,7 @@ namespace Lantean.QBitTorrentClient.Models
public bool? MaxInactiveSeedingTimeEnabled { get; set; }
[JsonPropertyName("max_ratio")]
public int? MaxRatio { get; set; }
public float? MaxRatio { get; set; }
[JsonPropertyName("max_ratio_act")]
public int? MaxRatioAct { get; set; }

11
nuget.config Normal file
View File

@@ -0,0 +1,11 @@
<configuration>
<packageSources>
<!-- Define package sources here -->
</packageSources>
<packageSourceMapping>
<!-- Optional source mapping -->
</packageSourceMapping>
<packageVersionOverride>
<package id="FluentAssertions" allowedVersions="[7.0.0,8.0.0)" />
</packageVersionOverride>
</configuration>

View File

@@ -1,14 +1,84 @@
# qbt-mud
# qbtmud
## To-Do
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
- Rename multiple files dialog
- ~~RSS feeds and dialogs~~
- ~~About~~
- ~~Context menu for files list/trackers list/peers list~~
- ~~Tag management page~~
- ~~Category management page~~
- ~~Update all tables to use DynamicTable~~
- ~~Log~~
- ~~Blocks~~
- ~~Search~~
## Features
qbtmud replicates all core features of the qBittorrent WebUI, including:
- **Torrent Management** Add, remove, and control torrents.
- **Tracker Control** View and manage trackers.
- **Peer Management** Monitor and manage peers connected to torrents.
- **File Prioritization** Select and prioritize specific files within a torrent.
- **Speed Limits** Set global and per-torrent speed limits.
- **RSS Integration** Subscribe to RSS feeds for automated torrent downloads.
- **Search Functionality** Integrated torrent search.
- **Sequential Downloading** Download files in order for media streaming.
- **Super Seeding Mode** Efficiently distribute torrents as an initial seeder.
- **IP Filtering** Improve security by filtering specific IP addresses.
- **IPv6 Support** Full support for IPv6 networks.
- **Bandwidth Scheduler** Schedule bandwidth limits.
- **WebUI Access** Remotely manage torrents through the WebUI.
![image](https://github.com/user-attachments/assets/c4e383fd-bff0-4367-b6de-79e19a632f11)
![image](https://github.com/user-attachments/assets/4ff56ed6-cc11-42cd-a070-23f086fd8821)
![image](https://github.com/user-attachments/assets/e321c5a2-ccf1-4205-828d-7ed7adade7dd)
For a detailed explanation of these features, refer to the [qBittorrent Options Guide](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
---
## Installation
To install qbtmud without building from source:
### 1. Download the Latest Release
- Go to the [qbtmud Releases](https://github.com/lantean-code/qbtmud/releases) page.
- Download the latest release archive for your operating system.
### 2. Extract the Archive
- Extract the contents of the downloaded archive to a directory of your choice.
### 3. Configure qBittorrent to Use qbtmud
- Open qBittorrent and navigate to `Tools` > `Options` > `Web UI`.
- Enable the option **"Use alternative WebUI"**.
- Set the **"Root Folder"** to the directory where you extracted qbtmud.
- Click **OK** to save the settings.
### 4. Access qbtmud
- Open your web browser and go to `http://localhost:8080` (or the port configured in qBittorrent).
For more detailed instructions, refer to the [Alternate WebUI Usage Guide](https://github.com/qbittorrent/qBittorrent/wiki/Alternate-WebUI-usage).
---
## Building from Source
To build qbtmud from source, you need to have the **.NET 9.0 SDK** installed on your system.
### 1. Clone the Repository
```sh
git clone https://github.com/lantean-code/qbtmud.git
cd qbtmud
```
### 2. Restore Dependencies
```sh
dotnet restore
```
### 3. Build and Publish the Application
```sh
dotnet publish --configuration Release
```
This will output the Web UI files to `Lantean.QBTMud\bin\Release\net9.0\publish\wwwroot`.
### 4. Configure qBittorrent to Use qbtmud
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
### 5. Run qbtmud
Navigate to the directory containing the built files and run the application using the appropriate command for your OS.
By following these steps, you can set up qbtmud to manage your qBittorrent server with an improved web interface, offering better functionality and usability.