19 Commits

Author SHA1 Message Date
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
55 changed files with 503 additions and 234 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,7 +1,7 @@
<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>
@@ -10,11 +10,14 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="FluentAssertions" Version="6.12.1" /> <PackageReference Include="FluentAssertions" Version="7.1.0" AllowedVersions="[5.0.0,7.*.*)" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MudBlazor" Version="8.2.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.0.1">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets> <IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference> </PackageReference>

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

@@ -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

@@ -63,7 +63,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; }
@@ -104,7 +104,7 @@ namespace Lantean.QBTMud.Components
_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 +441,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

@@ -1,7 +1,7 @@
<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>
@@ -12,13 +12,13 @@
<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.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" /> <PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="MudBlazor" Version="7.15.0" /> <PackageReference Include="MudBlazor" Version="8.2.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="2.1.0" /> <PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
<!-- added to fix vuln in dependency --> <!-- added to fix vuln in dependency -->
<PackageReference Include="System.Text.Json" Version="8.0.5" /> <PackageReference Include="System.Text.Json" Version="9.0.1" />
</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

@@ -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

@@ -90,9 +90,9 @@ namespace Lantean.QBTMud.Layout
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 +101,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

@@ -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

@@ -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

@@ -1,7 +1,7 @@
<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>

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>

77
readme.md Normal file
View File

@@ -0,0 +1,77 @@
# 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.
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 the Application
```sh
dotnet build --configuration Release
```
### 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.