33 Commits

Author SHA1 Message Date
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
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
ahjephson
7d62c9aecf Merge branch 'release/0.1.0' 2024-11-02 13:45:36 +00:00
ahjephson
b1e5424f55 Update rename files UI 2024-11-02 13:44:00 +00:00
ahjephson
66a6c2ca78 Move readme file 2024-11-02 10:47:46 +00:00
68 changed files with 614 additions and 312 deletions

View File

@@ -2,3 +2,78 @@
# IDE0290: Use primary constructor # IDE0290: Use primary constructor
csharp_style_prefer_primary_constructors = false csharp_style_prefer_primary_constructors = false
[*.cs]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.severity = suggestion
dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.symbols = private_or_internal_field
dotnet_naming_rule.private_or_internal_field_should_be_begins_with_underscore.style = begins_with_underscore
# Symbol specifications
dotnet_naming_symbols.private_or_internal_field.applicable_kinds = field
dotnet_naming_symbols.private_or_internal_field.applicable_accessibilities = internal, private, private_protected
dotnet_naming_symbols.private_or_internal_field.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_underscore.required_prefix = _
dotnet_naming_style.begins_with_underscore.required_suffix =
dotnet_naming_style.begins_with_underscore.word_separator =
dotnet_naming_style.begins_with_underscore.capitalization = camel_case
csharp_indent_labels = one_less_than_current
[*.{cs,vb}]
#### Naming styles ####
# Naming rules
dotnet_naming_rule.interface_should_be_begins_with_i.severity = suggestion
dotnet_naming_rule.interface_should_be_begins_with_i.symbols = interface
dotnet_naming_rule.interface_should_be_begins_with_i.style = begins_with_i
dotnet_naming_rule.types_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.types_should_be_pascal_case.symbols = types
dotnet_naming_rule.types_should_be_pascal_case.style = pascal_case
dotnet_naming_rule.non_field_members_should_be_pascal_case.severity = suggestion
dotnet_naming_rule.non_field_members_should_be_pascal_case.symbols = non_field_members
dotnet_naming_rule.non_field_members_should_be_pascal_case.style = pascal_case
# Symbol specifications
dotnet_naming_symbols.interface.applicable_kinds = interface
dotnet_naming_symbols.interface.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.interface.required_modifiers =
dotnet_naming_symbols.types.applicable_kinds = class, struct, interface, enum
dotnet_naming_symbols.types.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.types.required_modifiers =
dotnet_naming_symbols.non_field_members.applicable_kinds = property, event, method
dotnet_naming_symbols.non_field_members.applicable_accessibilities = public, internal, private, protected, protected_internal, private_protected
dotnet_naming_symbols.non_field_members.required_modifiers =
# Naming styles
dotnet_naming_style.begins_with_i.required_prefix = I
dotnet_naming_style.begins_with_i.required_suffix =
dotnet_naming_style.begins_with_i.word_separator =
dotnet_naming_style.begins_with_i.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_naming_style.pascal_case.required_prefix =
dotnet_naming_style.pascal_case.required_suffix =
dotnet_naming_style.pascal_case.word_separator =
dotnet_naming_style.pascal_case.capitalization = pascal_case
dotnet_style_operator_placement_when_wrapping = beginning_of_line
tab_width = 4
indent_size = 4
end_of_line = crlf

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig .editorconfig = .editorconfig
readme.md = readme.md
EndProjectSection EndProjectSection
EndProject EndProject
Global Global

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -9,16 +9,15 @@
<MudItem xs="12"> <MudItem xs="12">
<MudTextField T="string" Label="Search files" Value="Search" ValueChanged="SearchChanged" Variant="Variant.Outlined" /> <MudTextField T="string" Label="Search files" Value="Search" ValueChanged="SearchChanged" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12" lg="4">
<FieldSwitch Label="Use regular expressions" Value="UseRegex" ValueChanged="UseRegexChanged" /> <FieldSwitch Label="Use regular expressions" Value="UseRegex" ValueChanged="UseRegexChanged" />
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12" lg="4">
<FieldSwitch Label="Match all occurrences" Value="MatchAllOccurrences" ValueChanged="MatchAllOccurrencesChanged" /> <FieldSwitch Label="Match all occurrences" Value="MatchAllOccurrences" ValueChanged="MatchAllOccurrencesChanged" />
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12" lg="4">
<FieldSwitch Label="Case sensitive" Value="CaseSensitive" ValueChanged="CaseSensitiveChanged" /> <FieldSwitch Label="Case sensitive" Value="CaseSensitive" ValueChanged="CaseSensitiveChanged" />
</MudItem> </MudItem>
<MudDivider />
<MudItem xs="12"> <MudItem xs="12">
<MudTextField T="string" Label="Replacement" Value="Replacement" ValueChanged="ReplacementChanged" Variant="Variant.Outlined" /> <MudTextField T="string" Label="Replacement" Value="Replacement" ValueChanged="ReplacementChanged" Variant="Variant.Outlined" />
</MudItem> </MudItem>
@@ -29,20 +28,19 @@
<MudSelectItem T="AppliesTo" Value="AppliesTo.Extension">Extension</MudSelectItem> <MudSelectItem T="AppliesTo" Value="AppliesTo.Extension">Extension</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12" lg="4">
<FieldSwitch Label="Include files" Value="IncludeFiles" ValueChanged="IncludeFilesChanged" /> <FieldSwitch Label="Include files" Value="IncludeFiles" ValueChanged="IncludeFilesChanged" />
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12" lg="4">
<FieldSwitch Label="Include folders" Value="IncludeFolders" ValueChanged="IncludeFoldersChanged" /> <FieldSwitch Label="Include folders" Value="IncludeFolders" ValueChanged="IncludeFoldersChanged" />
</MudItem> </MudItem>
<MudItem xs="12" md="6"> <MudItem xs="12" lg="4">
<MudNumericField T="int" Label="Enumerate files" Value="FileEnumerationStart" ValueChanged="FileEnumerationStartChanged" Min="0" Variant="Variant.Outlined" /> <MudNumericField T="int" Label="Enumerate files" Value="FileEnumerationStart" ValueChanged="FileEnumerationStartChanged" Min="0" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudDivider />
<MudItem xs="12"> <MudItem xs="12">
<MudSelect T="bool" Label="Replace type" Value="ReplaceAll" ValueChanged="ReplaceAllChanged" Variant="Variant.Outlined"> <MudSelect T="bool" Label="Replace type" Value="ReplaceAll" ValueChanged="ReplaceAllChanged" Variant="Variant.Outlined">
<MudSelectItem T="bool" Value="true">Replace</MudSelectItem> <MudSelectItem T="bool" Value="false">Replace</MudSelectItem>
<MudSelectItem T="bool" Value="false">Replace all</MudSelectItem> <MudSelectItem T="bool" Value="true">Replace all</MudSelectItem>
</MudSelect> </MudSelect>
</MudItem> </MudItem>
</MudGrid> </MudGrid>

View File

@@ -6,7 +6,6 @@ using Lantean.QBTMud.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using static MudBlazor.Colors;
namespace Lantean.QBTMud.Components.Dialogs namespace Lantean.QBTMud.Components.Dialogs
{ {
@@ -31,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
protected ILocalStorageService LocalStorage { get; set; } = default!; protected ILocalStorageService LocalStorage { get; set; } = default!;
[CascadingParameter] [CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!; IMudDialogInstance MudDialog { get; set; } = default!;
[Parameter] [Parameter]
public string? Hash { get; set; } public string? Hash { get; set; }
@@ -484,19 +483,20 @@ namespace Lantean.QBTMud.Components.Dialogs
ReplaceAll, ReplaceAll,
FileEnumerationStart); FileEnumerationStart);
foreach (var (_, renamedFile) in renamedFiles) foreach (var (_, renamedFile) in renamedFiles.Where(f => !f.Value.IsFolder))
{ {
var oldPath = renamedFile.Path + renamedFile.OriginalName; var oldPath = renamedFile.Path + renamedFile.OriginalName;
var newPath = renamedFile.Path + renamedFile.NewName; var newPath = renamedFile.Path + renamedFile.NewName;
if (renamedFile.IsFolder)
{ await ApiClient.RenameFile(Hash, oldPath, newPath);
}
await ApiClient.RenameFolder(Hash, oldPath, newPath);
} foreach (var (_, renamedFile) in renamedFiles.Where(f => f.Value.IsFolder).OrderBy(f => f.Value.Path.Split(Extensions.DirectorySeparator)))
else {
{ var oldPath = renamedFile.Path + renamedFile.OriginalName;
await ApiClient.RenameFile(Hash, oldPath, newPath); var newPath = renamedFile.Path + renamedFile.NewName;
}
await ApiClient.RenameFolder(Hash, oldPath, newPath);
} }
MudDialog.Close(); MudDialog.Close();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
</ContextMenu> </ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true"> <MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" /> <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
@@ -22,6 +23,7 @@
<MudSpacer /> <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> <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> </MudToolBar>
</div>
<DynamicTable <DynamicTable
@ref="Table" @ref="Table"

View File

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

View File

@@ -92,7 +92,9 @@
<FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" /> <FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
</MudItem> </MudItem>
<MudItem xs="9"> <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>
<MudItem xs="3"> <MudItem xs="3">
<FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" /> <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 SlowTorrentUlRateThreshold { get; private set; }
protected int SlowTorrentInactiveTimer { get; private set; } protected int SlowTorrentInactiveTimer { get; private set; }
protected bool MaxRatioEnabled { 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 bool MaxSeedingTimeEnabled { get; private set; }
protected int MaxSeedingTime { get; private set; } protected int MaxSeedingTime { get; private set; }
protected int MaxRatioAct { get; private set; } protected int MaxRatioAct { get; private set; }
@@ -275,7 +275,7 @@
await PreferencesChanged.InvokeAsync(UpdatePreferences); await PreferencesChanged.InvokeAsync(UpdatePreferences);
} }
protected async Task MaxRatioChanged(int value) protected async Task MaxRatioChanged(float value)
{ {
MaxRatio = value; MaxRatio = value;
UpdatePreferences.MaxRatio = value; UpdatePreferences.MaxRatio = value;

View File

@@ -191,13 +191,13 @@ else if (RenderType == RenderType.MenuItems)
if (!action.Children.Any()) 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 @action.Text
</MudMenuItem> </MudMenuItem>
} }
else 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"> <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> <ActivatorContent>
@action.Text @action.Text

View File

@@ -12,10 +12,7 @@ namespace Lantean.QBTMud.Components
{ {
public partial class TorrentActions : IAsyncDisposable public partial class TorrentActions : IAsyncDisposable
{ {
private const int _defaultVersion = 5;
private bool _disposedValue; private bool _disposedValue;
private int? _version;
private List<UIAction>? _actions; private List<UIAction>? _actions;
@@ -63,7 +60,7 @@ namespace Lantean.QBTMud.Components
public QBitTorrentClient.Models.Preferences? Preferences { get; set; } public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[Parameter] [Parameter]
public MudDialogInstance? MudDialog { get; set; } public IMudDialogInstance? MudDialog { get; set; }
[Parameter] [Parameter]
public UIAction? ParentAction { get; set; } public UIAction? ParentAction { get; set; }
@@ -74,37 +71,14 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; } protected bool OverlayVisible { get; set; }
protected int MajorVersion protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
{
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 override void OnInitialized() protected override void OnInitialized()
{ {
_actions = _actions =
[ [
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)), 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("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("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), new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
@@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components
thereAreFirstLastPiecePrio = true; thereAreFirstLastPiecePrio = true;
} }
if (torrent.Progress != 1.0) // not downloaded if (torrent.Progress < 0.999999) // not downloaded
{ {
allAreDownloaded = false; allAreDownloaded = false;
} }

View File

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

View File

@@ -7,12 +7,6 @@ using MudBlazor.Utilities;
namespace Lantean.QBTMud.Components.UI 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 public partial class ContextMenu : MudComponentBase
{ {
private bool _open; private bool _open;
@@ -61,7 +55,7 @@ namespace Lantean.QBTMud.Components.UI
/// </summary> /// </summary>
[Parameter] [Parameter]
[Category(CategoryTypes.Menu.PopupAppearance)] [Category(CategoryTypes.Menu.PopupAppearance)]
public bool FullWidth { get; set; } public DropdownWidth RelativeWidth { get; set; }
/// <summary> /// <summary>
/// Sets the max height the menu can have when open. /// 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) if (!_isResized)
{ {
await DeterminePosition(); //await DeterminePosition();
} }
return Task.CompletedTask;
} }
private async Task DeterminePosition() //private async Task DeterminePosition()
{ //{
var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content"); // var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
double? contextMenuHeight = null; // double? contextMenuHeight = null;
double? contextMenuWidth = 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}"); // var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
if (popoverSize.Height > 0) // if (popoverSize.Height > 0)
{ // {
contextMenuHeight = popoverSize.Height; // contextMenuHeight = popoverSize.Height;
contextMenuWidth = popoverSize.Width; // contextMenuWidth = popoverSize.Width;
} // }
else // else
{ // {
return; // return;
} // }
// the bottom position of the popover will be rendered off screen // // the bottom position of the popover will be rendered off screen
if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height) // if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
{ // {
// adjust the top of the context menu // // adjust the top of the context menu
var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value)); // var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
_y -= overshoot; // _y -= overshoot;
if (_y - _diff + contextMenuHeight >= mainContentSize.Height) // if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
{ // {
MaxHeight = (int)(mainContentSize.Height - _y + _diff); // MaxHeight = (int)(mainContentSize.Height - _y + _diff);
} // }
} // }
if (_x + contextMenuWidth.Value > mainContentSize.Width) // if (_x + contextMenuWidth.Value > mainContentSize.Width)
{ // {
var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value)); // var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
_x -= overshoot; // _x -= overshoot;
} // }
SetPopoverStyle(_x, _y); // SetPopoverStyle(_x, _y);
_isResized = true; // _isResized = true;
await InvokeAsync(StateHasChanged); // await InvokeAsync(StateHasChanged);
} //}
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs) private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
{ {

View File

@@ -1,5 +1,5 @@
<div class="@Classname"> <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)) @if (!string.IsNullOrEmpty(Icon))
{ {
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" /> <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 _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort"; private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths"; private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
private readonly string _columnOrderStorageKey = $"DynamicTable{_typeName}.ColumnOrder";
[Inject] [Inject]
public ILocalStorageService LocalStorage { get; set; } = default!; public ILocalStorageService LocalStorage { get; set; } = default!;
@@ -82,6 +83,8 @@ namespace Lantean.QBTMud.Components.UI
private Dictionary<string, int?> _columnWidths = []; private Dictionary<string, int?> _columnWidths = [];
private Dictionary<string, int> _columnOrder = [];
private string? _sortColumn; private string? _sortColumn;
private SortDirection _sortDirection; private SortDirection _sortDirection;
@@ -165,8 +168,29 @@ namespace Lantean.QBTMud.Components.UI
protected IEnumerable<ColumnDefinition<T>> GetColumns() protected IEnumerable<ColumnDefinition<T>> GetColumns()
{ {
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter); 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)) if (_columnWidths.TryGetValue(column.Id, out var value))
{ {
column.Width = value; column.Width = value;
@@ -280,7 +304,7 @@ namespace Lantean.QBTMud.Components.UI
public async Task ShowColumnOptionsDialog() 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) if (result == default)
{ {
@@ -299,11 +323,17 @@ namespace Lantean.QBTMud.Components.UI
_columnWidths = result.ColumnWidths; _columnWidths = result.ColumnWidths;
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _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) private static string? GetColumnStyle(ColumnDefinition<T> column)

View File

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

View File

@@ -37,7 +37,7 @@ namespace Lantean.QBTMud.Helpers
{ {
time = TimeSpan.FromSeconds(seconds.Value); time = TimeSpan.FromSeconds(seconds.Value);
} }
catch (OverflowException) catch
{ {
return "∞"; return "∞";
} }
@@ -129,7 +129,7 @@ namespace Lantean.QBTMud.Helpers
return ""; return "";
} }
return Size(size); return Size(size, prefix, suffix);
} }
/// <summary> /// <summary>

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

View File

@@ -36,23 +36,23 @@
</CascadingValue> </CascadingValue>
</CascadingValue> </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) @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 /> <MudSpacer />
<MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText> <MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" /> <MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText> <MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" /> <MudDivider Vertical="true" Class="d-none d-sm-flex" />
@{ @{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus); var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
} }
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="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)" /> <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" /> <MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
<MudText Class="mr-1 mb-1"> <MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s") @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")

View File

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

View File

@@ -20,16 +20,18 @@
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" /> <MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge> </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" /> <Menu @ref="Menu" />
</MudAppBar> </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" /> <ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer> </MudDrawer>
<CascadingValue Value="Theme"> <CascadingValue Value="Theme">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode"> <CascadingValue Value="IsDarkMode" Name="IsDarkMode">
<CascadingValue Value="Menu"> <CascadingValue Value="Menu">
@Body <CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>

View File

@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
private bool _disposedValue; private bool _disposedValue;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Inject] [Inject]
private IBrowserViewportService BrowserViewportService { get; set; } = default!; private IBrowserViewportService BrowserViewportService { get; set; } = default!;
@@ -78,21 +75,21 @@ namespace Lantean.QBTMud.Layout
{ {
IsDarkMode = isDarkMode.Value; IsDarkMode = isDarkMode.Value;
} }
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged); await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true); await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
} }
protected Task OnSystemPreferenceChanged(bool value) protected Task OnSystemDarkModeChanged(bool value)
{ {
IsDarkMode = value; IsDarkMode = value;
return Task.CompletedTask; 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; DrawerOpen = false;
} }
@@ -101,7 +98,17 @@ namespace Lantean.QBTMud.Layout
DrawerOpen = true; 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() protected void ToggleErrorDrawer()

View File

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

View File

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

View File

@@ -12,89 +12,96 @@
<MudTabs Elevation="2" ApplyEffectsToContainer="true"> <MudTabs Elevation="2" ApplyEffectsToContainer="true">
<MudTabPanel Text="About"> <MudTabPanel Text="About">
<div class="d-flex gap-4"> <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop" Height="162" Width="94" /> <MudGrid Class="mt-0 mb-4">
<MudGrid Class="mx-0 mt-0 mb-3"> <MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
<MudItem xs="12"> <MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
<div class="d-flex gap-3"> Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop"
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None" Alt="QBT" Height="32" Width="32" /><MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText> 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> </div>
</MudItem> </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> </MudGrid>
</div> </MudContainer>
</MudTabPanel> </MudTabPanel>
<MudTabPanel Text="Authors"> <MudTabPanel Text="Authors">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3"> <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"> <MudGrid Class="mt-0 mb-4">
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">Name</MudText> <MudText Typo="Typo.h6">Name</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Sledgehammer999</MudText> <MudText Typo="Typo.body1">Sledgehammer999</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">Nationality</MudText> <MudText Typo="Typo.h6">Nationality</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Greece</MudText> <MudText Typo="Typo.body1">Greece</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">E-mail</MudText> <MudText Typo="Typo.h6">E-mail</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink> <MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
</MudItem> </MudItem>
</MudGrid> </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"> <MudGrid Class="mt-0 mb-4">
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">Name</MudText> <MudText Typo="Typo.h6">Name</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Christophe Dumez</MudText> <MudText Typo="Typo.body1">Christophe Dumez</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">Nationality</MudText> <MudText Typo="Typo.h6">Nationality</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudText Typo="Typo.body1">France</MudText> <MudText Typo="Typo.body1">France</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="12" md="2">
<MudText Typo="Typo.body1">E-mail</MudText> <MudText Typo="Typo.h6">E-mail</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="12" md="10">
<MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink> <MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
</MudItem> </MudItem>
</MudGrid> </MudGrid>
@@ -118,7 +125,7 @@
(the list might not be up to date) (the list might not be up to date)
</MudText> </MudText>
<MudList T="string" ReadOnly> <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>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>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@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> <MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
<MudGrid Class="mt-1 mb-4"> <MudGrid Class="mt-1 mb-4">
<MudItem xs="2"> <MudItem xs="3">
<MudText Typo="Typo.body1">Qt</MudText> <MudText Typo="Typo.body1">Qt</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="9">
<MudText Typo="Typo.body1">@QtVersion</MudText> <MudText Typo="Typo.body1">@QtVersion</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="3">
<MudText Typo="Typo.body1">Libtorrent</MudText> <MudText Typo="Typo.body1">Libtorrent</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="9">
<MudText Typo="Typo.body1">@LibtorrentVersion</MudText> <MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="3">
<MudText Typo="Typo.body1">Boost</MudText> <MudText Typo="Typo.body1">Boost</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="9">
<MudText Typo="Typo.body1">@BoostVersion</MudText> <MudText Typo="Typo.body1">@BoostVersion</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="3">
<MudText Typo="Typo.body1">OpenSSL</MudText> <MudText Typo="Typo.body1">OpenSSL</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="9">
<MudText Typo="Typo.body1">@OpensslVersion</MudText> <MudText Typo="Typo.body1">@OpensslVersion</MudText>
</MudItem> </MudItem>
<MudItem xs="2"> <MudItem xs="3">
<MudText Typo="Typo.body1">zlib</MudText> <MudText Typo="Typo.body1">zlib</MudText>
</MudItem> </MudItem>
<MudItem xs="10"> <MudItem xs="9">
<MudText Typo="Typo.body1">@ZlibVersion</MudText> <MudText Typo="Typo.body1">@ZlibVersion</MudText>
</MudItem> </MudItem>
</MudGrid> </MudGrid>

View File

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

View File

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

View File

@@ -18,7 +18,7 @@
<MudItem xs="12" md="4"> <MudItem xs="12" md="4">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" /> <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudItem xs="2" md="3"> <MudItem xs="12" md="3">
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined"> <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
@foreach (var (value, name) in Categories) @foreach (var (value, name) in Categories)
{ {
@@ -30,17 +30,21 @@
} }
</MudSelect> </MudSelect>
</MudItem> </MudItem>
<MudItem xs="2" md="3"> <MudItem xs="12" md="3">
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined"> <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
<MudSelectItem Value="@("all")">All</MudSelectItem> <MudSelectItem Value="@("all")">All</MudSelectItem>
<MudDivider /> @if (Plugins.Count > 0)
{
<MudDivider />
}
@foreach (var (value, name) in Plugins) @foreach (var (value, name) in Plugins)
{ {
<MudSelectItem Value="value">@name</MudSelectItem> <MudSelectItem Value="value">@name</MudSelectItem>
} }
</MudSelect> </MudSelect>
</MudItem> </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> <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> </MudItem>

View File

@@ -1,12 +1,13 @@
@page "/" @page "/"
@layout ListLayout @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> <MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
<MudDivider /> <MudDivider />
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" /> <TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
</ContextMenu> </ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true"> <MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" /> <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" /> <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
@@ -18,6 +19,7 @@
<MudSpacer /> <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> <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> </MudToolBar>
</div>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0"> <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable <DynamicTable

View File

@@ -193,7 +193,7 @@ namespace Lantean.QBTMud.Pages
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } = public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
[ [
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("#", t => t.Priority), 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>("Name", t => t.Name, width: 400),
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)), 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), 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 public class DataManager : IDataManager
{ {
private static readonly Status[] _statuses = Enum.GetValues<Status>(); private static Status[]? _statusArray = null;
public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers) public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers)
{ {
@@ -25,8 +25,9 @@ namespace Lantean.QBTMud.Services
return peerList; 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); var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
if (mainData.Torrents is not null) if (mainData.Torrents is not null)
{ {
@@ -87,8 +88,9 @@ namespace Lantean.QBTMud.Services
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet()); categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
} }
var statusState = new Dictionary<string, HashSet<string>>(_statuses.Length + 2); var statuses = GetStatuses(majorVersion).ToArray();
foreach (var status in _statuses) 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()); statusState.Add(status.ToString(), torrents.Values.Where(t => FilterHelper.FilterStatus(t, status)).ToHashesHashSet());
} }
@@ -101,7 +103,7 @@ namespace Lantean.QBTMud.Services
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet()); 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; return torrentList;
} }
@@ -206,7 +208,7 @@ namespace Lantean.QBTMud.Services
{ {
foreach (var (url, hashes) in mainData.Trackers) 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); torrentList.Trackers.Add(url, hashes);
} }
@@ -225,7 +227,7 @@ namespace Lantean.QBTMud.Services
{ {
var newTorrent = CreateTorrent(hash, torrent); var newTorrent = CreateTorrent(hash, torrent);
torrentList.Torrents.Add(hash, newTorrent); torrentList.Torrents.Add(hash, newTorrent);
AddTorrentToStates(torrentList, hash); AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
} }
else else
{ {
@@ -241,7 +243,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]; var torrent = torrentList.Torrents[hash];
@@ -271,7 +273,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); 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)); torrentList.StatusState[status.ToString()].AddIfTrue(hash, FilterHelper.FilterStatus(torrent, status));
} }
@@ -289,6 +291,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) private static void UpdateTorrentStates(MainData torrentList, string hash)
{ {
var torrent = torrentList.Torrents[hash]; var torrent = torrentList.Torrents[hash];
@@ -317,7 +338,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); 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)); torrentList.StatusState[status.ToString()].AddIfTrueOrRemove(hash, FilterHelper.FilterStatus(torrent, status));
} }
@@ -361,7 +382,7 @@ namespace Lantean.QBTMud.Services
categoryState.RemoveIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories)); 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)) if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
{ {

View File

@@ -4,7 +4,7 @@ namespace Lantean.QBTMud.Services
{ {
public interface IDataManager 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); Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);

View File

@@ -1,14 +0,0 @@
# qbt-mud
## To-Do
- 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~~

View File

@@ -155,7 +155,7 @@ code {
} }
.torrent-list .mud-table-container { .torrent-list .mud-table-container {
height: calc(100vh - 149px); height: calc(100vh - 160px);
} }
.file-list .mud-table-container { .file-list .mud-table-container {
@@ -240,4 +240,19 @@ td .folder-button {
.mud-dialog .mud-dialog-content { .mud-dialog .mud-dialog-content {
padding-top: 4px !important; 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.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <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="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 href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="css/app.css" /> <link rel="stylesheet" href="./css/app.css" />
<link rel="icon" type="image/png" href="images/qbittorrent32.png" /> <link rel="icon" type="image/png" href="images/qbittorrent32.png" />
<link rel="icon" href="images/qbittorrent-tray.svg"> <link rel="icon" href="./images/qbittorrent-tray.svg">
<link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000"> <link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
<link rel="apple-touch-icon" href="images/qbittorrent32.png"> <link rel="apple-touch-icon" href="./images/qbittorrent32.png">
</head> </head>
<body> <body>
@@ -31,10 +31,10 @@
<a href="" class="reload">Reload</a> <a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a> <a class="dismiss">🗙</a>
</div> </div>
<script src="_framework/blazor.webassembly.js"></script> <script src="./_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script> <script src="./_content/MudBlazor/MudBlazor.min.js"></script>
<script src="js/piecesbar.js"></script> <script src="./js/piecesbar.js"></script>
<script src="js/interop.js"></script> <script src="./js/interop.js"></script>
</body> </body>
</html> </html>

View File

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

View File

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

View File

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

84
readme.md Normal file
View File

@@ -0,0 +1,84 @@
# qbtmud
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
## 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.