mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-23 04:52:22 +00:00
Compare commits
35 Commits
v0.1.0
...
2c744cd972
Author | SHA1 | Date | |
---|---|---|---|
|
2c744cd972 | ||
|
b02bb7cfae | ||
|
e4dac8556e | ||
|
a9a8a4eba8 | ||
|
bb524450f0 | ||
|
b24ae440d4 | ||
|
bb90ce5216 | ||
|
1cf9f97187 | ||
|
4f9129fd46 | ||
|
9a9d2c2ee2 | ||
|
736bc46745 | ||
|
23ae19c4c7 | ||
|
603470eb30 | ||
|
27c2406340 | ||
|
4578dcc11f | ||
|
3215fa3936 | ||
|
78e62f31d0 | ||
|
e23842fcb0 | ||
|
411c7f87cc | ||
|
4098f8f5a9 | ||
|
12f81c5978 | ||
|
717738d720 | ||
|
885c34c8cf | ||
|
ef3c68a6aa | ||
|
a29e64fc1b | ||
|
e55955c75e | ||
|
aa80396862 | ||
|
30ced3293c | ||
|
c54f73a517 | ||
|
bad509e40f | ||
|
6a0796ef20 | ||
|
dc4b515763 | ||
|
938702a7b3 | ||
|
6ca1c6edd4 | ||
|
24eb5cf5e9 |
4
.github/workflows/dotnet.yml
vendored
4
.github/workflows/dotnet.yml
vendored
@@ -21,12 +21,12 @@ jobs:
|
||||
- name: Setup .NET
|
||||
uses: actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '8.0.x'
|
||||
dotnet-version: '9.0.x'
|
||||
|
||||
- name: Install GitVersion
|
||||
uses: gittools/actions/gitversion/setup@v3.0.0
|
||||
with:
|
||||
versionSpec: '6.x'
|
||||
versionSpec: '6.0.0'
|
||||
|
||||
- name: Determine Version
|
||||
id: gitversion
|
||||
|
3
.gitignore
vendored
3
.gitignore
vendored
@@ -360,4 +360,5 @@ MigrationBackup/
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
/output
|
||||
|
@@ -1,24 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" Version="6.12.1" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.11.1" />
|
||||
<PackageReference Include="AwesomeAssertions" Version="9.0.0" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.0" />
|
||||
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2">
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Test
|
||||
Test2(a => a.Name);
|
||||
}
|
||||
|
||||
private void Test2(Expression<Func<TestClass, object>> expr)
|
||||
private void Test2(Expression<Func<TestClass, object?>> expr)
|
||||
{
|
||||
var body = expr.Body;
|
||||
}
|
||||
@@ -38,7 +38,7 @@ namespace Lantean.QBTMud.Test
|
||||
|
||||
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
|
||||
|
||||
Expression<Func<TestClass, object>> expr2 = a => a.Name;
|
||||
Expression<Func<TestClass, object?>> expr2 = a => a.Name;
|
||||
|
||||
var x = l.Compile();
|
||||
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
|
||||
@@ -58,9 +58,9 @@ namespace Lantean.QBTMud.Test
|
||||
|
||||
public class TestClass
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public string? Name { get; set; }
|
||||
|
||||
public string Description { get; set; }
|
||||
public string? Description { get; set; }
|
||||
|
||||
public long Value { get; set; }
|
||||
}
|
||||
|
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class AddPeerDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
public IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected HashSet<PeerId> Peers { get; } = [];
|
||||
|
||||
|
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected HashSet<string> Tags { get; } = [];
|
||||
|
||||
|
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class AddTorrentFileDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<IBrowserFile> Files { get; set; } = [];
|
||||
|
||||
|
@@ -18,7 +18,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IKeyboardService KeyboardService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Url { get; set; }
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPosition="LabelPosition.End" />
|
||||
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPlacement="Placement.End" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
<MudCollapse Expanded="Expanded">
|
||||
|
@@ -1,6 +1,7 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
|
||||
namespace Lantean.QBTMud.Components.Dialogs
|
||||
{
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class AddTrackerDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected HashSet<string> Trackers { get; } = [];
|
||||
|
||||
|
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private string _savePath = string.Empty;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IApiClient ApiClient { get; set; } = default!;
|
||||
|
@@ -5,12 +5,13 @@
|
||||
<DialogContent>
|
||||
<MudCard Class="w-100" Elevation="0">
|
||||
<MudGrid>
|
||||
@for (var i = 0; i < Columns.Count; i++)
|
||||
@for (var i = 0; i < OrderedColumns.Length; i++)
|
||||
{
|
||||
var column = Columns[i];
|
||||
var item = OrderedColumns[i];
|
||||
var column = Columns.First(c => c.Id == item);
|
||||
var index = i;
|
||||
<MudItem xs="7">
|
||||
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPosition="LabelPosition.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
|
||||
<MudCheckBox T="bool" ValueChanged="@(c => SetSelected(c, column.Id))" Label="@column.Header" LabelPlacement="Placement.End" Value="@(SelectedColumnsInternal.Contains(column.Id))" />
|
||||
</MudItem>
|
||||
<MudItem xs="3">
|
||||
<MudTextField T="string" Value="@(GetValue(column.Width, column.Id))" ValueChanged="@(c => SetWidth(c, column.Id))" Label="Width" Variant="Variant.Text" HelperText="px" Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Outlined.WidthNormal" OnAdornmentClick="@(c => SetWidth("auto", column.Id))" />
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ColumnOptionsDialog<T>
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
private IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
@@ -20,10 +20,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
[Parameter]
|
||||
public Dictionary<string, int?> Widths { get; set; } = [];
|
||||
|
||||
[Parameter]
|
||||
public Dictionary<string, int> Order { get; set; } = [];
|
||||
|
||||
protected HashSet<string> SelectedColumnsInternal { get; set; } = [];
|
||||
|
||||
protected Dictionary<string, int?> WidthsInternal { get; set; } = [];
|
||||
|
||||
protected Dictionary<string, int> OrderInternal { get; set; } = [];
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
if (SelectedColumnsInternal.Count == 0)
|
||||
@@ -51,6 +56,25 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
WidthsInternal[width.Key] = width.Value;
|
||||
}
|
||||
}
|
||||
|
||||
if (OrderInternal.Count == 0)
|
||||
{
|
||||
if (Order.Count == 0)
|
||||
{
|
||||
for (int i = 0; i < Columns.Count; i++)
|
||||
{
|
||||
var column = Columns[i];
|
||||
OrderInternal.Add(column.Id, i);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var order in Order)
|
||||
{
|
||||
OrderInternal[order.Key] = order.Value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected void SetSelected(bool selected, string id)
|
||||
@@ -101,7 +125,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
return;
|
||||
}
|
||||
|
||||
(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
|
||||
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
|
||||
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index - 1).Key;
|
||||
|
||||
OrderInternal[otherId] = index;
|
||||
OrderInternal[currentId] = index - 1;
|
||||
|
||||
//(Columns[index], Columns[index - 1]) = (Columns[index - 1], Columns[index]);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected void MoveDown(int index)
|
||||
@@ -111,7 +143,15 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
return;
|
||||
}
|
||||
|
||||
(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
|
||||
var currentId = OrderInternal.FirstOrDefault(o => o.Value == index).Key;
|
||||
var otherId = OrderInternal.FirstOrDefault(o => o.Value == index + 1).Key;
|
||||
|
||||
OrderInternal[otherId] = index;
|
||||
OrderInternal[currentId] = index + 1;
|
||||
|
||||
//(Columns[index], Columns[index + 1]) = (Columns[index + 1], Columns[index]);
|
||||
|
||||
StateHasChanged();
|
||||
}
|
||||
|
||||
protected string GetValue(int? value, string columnId)
|
||||
@@ -134,6 +174,13 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
return value.Value.ToString();
|
||||
}
|
||||
|
||||
private string[] OrderedColumns => GetOrderedColumns();
|
||||
|
||||
private string[] GetOrderedColumns()
|
||||
{
|
||||
return OrderInternal.OrderBy(x => x.Value).Select(x => x.Key).ToArray();
|
||||
}
|
||||
|
||||
protected void Cancel()
|
||||
{
|
||||
MudDialog.Cancel();
|
||||
@@ -141,7 +188,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
|
||||
protected void Submit()
|
||||
{
|
||||
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal)));
|
||||
MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, WidthsInternal, OrderInternal)));
|
||||
}
|
||||
|
||||
protected override Task Submit(KeyboardEvent keyboardEvent)
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ConfirmDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string Content { get; set; } = default!;
|
||||
|
@@ -6,7 +6,7 @@
|
||||
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPosition="LabelPosition.End" />
|
||||
<MudCheckBox Label="Also permanently delete the files" @bind-Value="DeleteFiles" LabelPlacement="Placement.End" />
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</DialogContent>
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class DeleteDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public int Count { get; set; }
|
||||
|
@@ -6,7 +6,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ExceptionDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public Exception? Exception { get; set; }
|
||||
|
@@ -11,7 +11,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private static readonly IReadOnlyList<PropertyInfo> _properties = typeof(T).GetProperties(BindingFlags.Instance | BindingFlags.Public);
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
protected IReadOnlyList<PropertyInfo> Columns => _properties;
|
||||
|
||||
|
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<string> Hashes { get; set; } = [];
|
||||
|
@@ -14,7 +14,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public IEnumerable<string> Hashes { get; set; } = [];
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class MultipleFieldDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string Label { get; set; } = default!;
|
||||
|
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class NumericFieldDialog<T> where T : struct, INumber<T>
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Label { get; set; }
|
||||
|
@@ -6,7 +6,6 @@ using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System.Collections.ObjectModel;
|
||||
using static MudBlazor.Colors;
|
||||
|
||||
namespace Lantean.QBTMud.Components.Dialogs
|
||||
{
|
||||
@@ -31,7 +30,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
protected ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Hash { get; set; }
|
||||
|
@@ -10,7 +10,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
private readonly List<string> _unsavedRuleNames = [];
|
||||
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
protected IDialogService DialogService { get; set; } = default!;
|
||||
|
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class ShareRatioDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Label { get; set; }
|
||||
|
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class SliderFieldDialog<T> where T : struct, INumber<T>
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Label { get; set; }
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class StringFieldDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public string? Label { get; set; }
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class SubMenuDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
public UIAction? ParentAction { get; set; }
|
||||
|
@@ -7,7 +7,7 @@ namespace Lantean.QBTMud.Components.Dialogs
|
||||
public partial class TorrentOptionsDialog
|
||||
{
|
||||
[CascadingParameter]
|
||||
public MudDialogInstance MudDialog { get; set; } = default!;
|
||||
IMudDialogInstance MudDialog { get; set; } = default!;
|
||||
|
||||
[Parameter]
|
||||
[EditorRequired]
|
||||
|
@@ -2,6 +2,7 @@
|
||||
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
|
||||
</ContextMenu>
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
|
||||
<MudDivider Vertical="true" />
|
||||
@@ -22,6 +23,7 @@
|
||||
<MudSpacer />
|
||||
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
<DynamicTable
|
||||
@ref="Table"
|
||||
|
@@ -8,6 +8,7 @@ using Lantean.QBTMud.Models;
|
||||
using Lantean.QBTMud.Services;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using MudBlazor;
|
||||
using System;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Net;
|
||||
|
||||
@@ -20,6 +21,9 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
private readonly CancellationTokenSource _timerCancellationToken = new();
|
||||
private bool _disposedValue;
|
||||
private static readonly ReadOnlyCollection<ContentItem> EmptyContentItems = new ReadOnlyCollection<ContentItem>(Array.Empty<ContentItem>());
|
||||
private ReadOnlyCollection<ContentItem> _visibleFiles = EmptyContentItems;
|
||||
private bool _filesDirty = true;
|
||||
|
||||
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
|
||||
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
|
||||
@@ -102,6 +106,7 @@ namespace Lantean.QBTMud.Components
|
||||
if (_filterDefinitions is null)
|
||||
{
|
||||
Filters = null;
|
||||
MarkFilesDirty();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -113,11 +118,13 @@ namespace Lantean.QBTMud.Components
|
||||
}
|
||||
|
||||
Filters = filters;
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
protected void RemoveFilter()
|
||||
{
|
||||
Filters = null;
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
@@ -157,6 +164,7 @@ namespace Lantean.QBTMud.Components
|
||||
protected void SearchTextChanged(string value)
|
||||
{
|
||||
SearchText = value;
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
|
||||
@@ -197,6 +205,7 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
|
||||
{
|
||||
var hasUpdates = false;
|
||||
if (Active && Hash is not null)
|
||||
{
|
||||
IReadOnlyList<QBitTorrentClient.Models.FileData> files;
|
||||
@@ -213,14 +222,20 @@ namespace Lantean.QBTMud.Components
|
||||
if (FileList is null)
|
||||
{
|
||||
FileList = DataManager.CreateContentsList(files);
|
||||
hasUpdates = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DataManager.MergeContentsList(files, FileList);
|
||||
hasUpdates = DataManager.MergeContentsList(files, FileList);
|
||||
}
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
if (hasUpdates)
|
||||
{
|
||||
MarkFilesDirty();
|
||||
PruneSelectionIfMissing();
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -246,6 +261,8 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
var contents = await ApiClient.GetTorrentContents(Hash);
|
||||
FileList = DataManager.CreateContentsList(contents);
|
||||
MarkFilesDirty();
|
||||
PruneSelectionIfMissing();
|
||||
|
||||
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
|
||||
if (expandedNodes is not null)
|
||||
@@ -256,6 +273,8 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
ExpandedNodes.Clear();
|
||||
}
|
||||
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
|
||||
@@ -320,11 +339,13 @@ namespace Lantean.QBTMud.Components
|
||||
protected void SortColumnChanged(string sortColumn)
|
||||
{
|
||||
_sortColumn = sortColumn;
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
protected void SortDirectionChanged(SortDirection sortDirection)
|
||||
{
|
||||
_sortDirection = sortDirection;
|
||||
MarkFilesDirty();
|
||||
}
|
||||
|
||||
protected void SelectedItemChanged(ContentItem item)
|
||||
@@ -343,6 +364,7 @@ namespace Lantean.QBTMud.Components
|
||||
ExpandedNodes.Add(contentItem.Name);
|
||||
}
|
||||
|
||||
MarkFilesDirty();
|
||||
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
|
||||
}
|
||||
|
||||
@@ -368,44 +390,6 @@ namespace Lantean.QBTMud.Components
|
||||
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
|
||||
}
|
||||
|
||||
private IEnumerable<ContentItem> GetChildren(ContentItem folder, int level)
|
||||
{
|
||||
level++;
|
||||
var descendantsKey = folder.GetDescendantsKey(level);
|
||||
|
||||
foreach (var item in FileList!.Values.Where(f => f.Name.StartsWith(descendantsKey) && f.Level == level).OrderByDirection(_sortDirection, GetSortSelector()))
|
||||
{
|
||||
if (item.IsFolder)
|
||||
{
|
||||
var descendants = GetChildren(item, level);
|
||||
// if the filter returns some results then show folder item
|
||||
if (descendants.Any())
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
|
||||
// if the folder is not expanded - don't return children
|
||||
if (!ExpandedNodes.Contains(item.Name))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// then show children
|
||||
foreach (var descendant in descendants)
|
||||
{
|
||||
yield return descendant;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterContentItem(item))
|
||||
{
|
||||
yield return item;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool FilterContentItem(ContentItem item)
|
||||
{
|
||||
if (Filters is not null)
|
||||
@@ -429,38 +413,130 @@ namespace Lantean.QBTMud.Components
|
||||
}
|
||||
|
||||
private ReadOnlyCollection<ContentItem> GetFiles()
|
||||
{
|
||||
if (!_filesDirty)
|
||||
{
|
||||
return _visibleFiles;
|
||||
}
|
||||
|
||||
_visibleFiles = BuildVisibleFiles();
|
||||
_filesDirty = false;
|
||||
|
||||
return _visibleFiles;
|
||||
}
|
||||
|
||||
private ReadOnlyCollection<ContentItem> BuildVisibleFiles()
|
||||
{
|
||||
if (FileList is null || FileList.Values.Count == 0)
|
||||
{
|
||||
return new ReadOnlyCollection<ContentItem>([]);
|
||||
return EmptyContentItems;
|
||||
}
|
||||
|
||||
var maxLevel = FileList.Values.Max(f => f.Level);
|
||||
// this is a flat file structure
|
||||
if (maxLevel == 0)
|
||||
var lookup = BuildChildrenLookup();
|
||||
if (!lookup.TryGetValue(string.Empty, out var roots))
|
||||
{
|
||||
return FileList.Values.Where(FilterContentItem).OrderByDirection(_sortDirection, GetSortSelector()).ToList().AsReadOnly();
|
||||
return EmptyContentItems;
|
||||
}
|
||||
|
||||
var list = new List<ContentItem>();
|
||||
var sortSelector = GetSortSelector();
|
||||
var orderedRoots = roots.OrderByDirection(_sortDirection, sortSelector).ToList();
|
||||
var result = new List<ContentItem>(FileList.Values.Count);
|
||||
|
||||
var rootItems = FileList.Values.Where(c => c.Level == 0).OrderByDirection(_sortDirection, GetSortSelector()).ToList();
|
||||
foreach (var item in rootItems)
|
||||
foreach (var item in orderedRoots)
|
||||
{
|
||||
list.Add(item);
|
||||
|
||||
if (item.IsFolder && ExpandedNodes.Contains(item.Name))
|
||||
if (item.IsFolder)
|
||||
{
|
||||
var level = 0;
|
||||
var descendants = GetChildren(item, level);
|
||||
foreach (var descendant in descendants)
|
||||
result.Add(item);
|
||||
|
||||
if (!ExpandedNodes.Contains(item.Name))
|
||||
{
|
||||
list.Add(descendant);
|
||||
continue;
|
||||
}
|
||||
|
||||
var descendants = GetVisibleDescendants(item, lookup, sortSelector);
|
||||
result.AddRange(descendants);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (FilterContentItem(item))
|
||||
{
|
||||
result.Add(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list.AsReadOnly();
|
||||
return new ReadOnlyCollection<ContentItem>(result);
|
||||
}
|
||||
|
||||
private Dictionary<string, List<ContentItem>> BuildChildrenLookup()
|
||||
{
|
||||
var lookup = new Dictionary<string, List<ContentItem>>(FileList!.Count);
|
||||
|
||||
foreach (var item in FileList!.Values)
|
||||
{
|
||||
var parentPath = item.Level == 0 ? string.Empty : item.Name.GetDirectoryPath();
|
||||
if (!lookup.TryGetValue(parentPath, out var children))
|
||||
{
|
||||
children = [];
|
||||
lookup[parentPath] = children;
|
||||
}
|
||||
|
||||
children.Add(item);
|
||||
}
|
||||
|
||||
return lookup;
|
||||
}
|
||||
|
||||
private List<ContentItem> GetVisibleDescendants(ContentItem folder, Dictionary<string, List<ContentItem>> lookup, Func<ContentItem, object?> sortSelector)
|
||||
{
|
||||
if (!lookup.TryGetValue(folder.Name, out var children))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
var orderedChildren = children.OrderByDirection(_sortDirection, sortSelector).ToList();
|
||||
var visible = new List<ContentItem>();
|
||||
|
||||
foreach (var child in orderedChildren)
|
||||
{
|
||||
if (child.IsFolder)
|
||||
{
|
||||
var descendants = GetVisibleDescendants(child, lookup, sortSelector);
|
||||
if (descendants.Count != 0)
|
||||
{
|
||||
visible.Add(child);
|
||||
|
||||
if (ExpandedNodes.Contains(child.Name))
|
||||
{
|
||||
visible.AddRange(descendants);
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (FilterContentItem(child))
|
||||
{
|
||||
visible.Add(child);
|
||||
}
|
||||
}
|
||||
|
||||
return visible;
|
||||
}
|
||||
|
||||
private void MarkFilesDirty()
|
||||
{
|
||||
_filesDirty = true;
|
||||
}
|
||||
|
||||
private void PruneSelectionIfMissing()
|
||||
{
|
||||
if (SelectedItem is not null && (FileList is null || !FileList.ContainsKey(SelectedItem.Name)))
|
||||
{
|
||||
SelectedItem = null;
|
||||
}
|
||||
|
||||
if (ContextMenuItem is not null && (FileList is null || !FileList.ContainsKey(ContextMenuItem.Name)))
|
||||
{
|
||||
ContextMenuItem = null;
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task DoNotDownloadLessThan100PercentAvailability()
|
||||
@@ -552,4 +628,4 @@ namespace Lantean.QBTMud.Components
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<ContentItem>("Availability", c => c.Availability, c => c.Availability.ToString("0.00")),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
|
||||
<ContextMenu @ref="StatusContextMenu" Dense="true" AdjustmentY="-60">
|
||||
@TorrentControls(_statusType)
|
||||
</ContextMenu>
|
||||
|
||||
|
@@ -92,7 +92,9 @@
|
||||
<FieldSwitch Label="When ratio reaches" Value="MaxRatioEnabled" ValueChanged="MaxRatioEnabledChanged" />
|
||||
</MudItem>
|
||||
<MudItem xs="9">
|
||||
<MudNumericField T="int" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged" Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined" Validation="MaxRatioValidation" />
|
||||
<MudNumericField T="float" Label="" Value="MaxRatio" ValueChanged="MaxRatioChanged"
|
||||
Disabled="@(!MaxRatioEnabled)" Min="0" Max="9998" Variant="Variant.Outlined"
|
||||
Validation="MaxRatioValidation" />
|
||||
</MudItem>
|
||||
<MudItem xs="3">
|
||||
<FieldSwitch Label="When total seeding time reaches" Value="MaxSeedingTimeEnabled" ValueChanged="MaxSeedingTimeEnabledChanged" />
|
||||
|
@@ -17,7 +17,7 @@
|
||||
protected int SlowTorrentUlRateThreshold { get; private set; }
|
||||
protected int SlowTorrentInactiveTimer { get; private set; }
|
||||
protected bool MaxRatioEnabled { get; private set; }
|
||||
protected int MaxRatio { get; private set; }
|
||||
protected float MaxRatio { get; private set; }
|
||||
protected bool MaxSeedingTimeEnabled { get; private set; }
|
||||
protected int MaxSeedingTime { get; private set; }
|
||||
protected int MaxRatioAct { get; private set; }
|
||||
@@ -275,7 +275,7 @@
|
||||
await PreferencesChanged.InvokeAsync(UpdatePreferences);
|
||||
}
|
||||
|
||||
protected async Task MaxRatioChanged(int value)
|
||||
protected async Task MaxRatioChanged(float value)
|
||||
{
|
||||
MaxRatio = value;
|
||||
UpdatePreferences.MaxRatio = value;
|
||||
|
@@ -191,13 +191,13 @@ else if (RenderType == RenderType.MenuItems)
|
||||
|
||||
if (!action.Children.Any())
|
||||
{
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled" Class="icon-menu-dense">
|
||||
@action.Text
|
||||
</MudMenuItem>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))">
|
||||
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))" Class="icon-menu-dense">
|
||||
<MudMenu ListClass="unselectable" Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" Ripple="false" Class="sub-menu">
|
||||
<ActivatorContent>
|
||||
@action.Text
|
||||
|
@@ -12,10 +12,7 @@ namespace Lantean.QBTMud.Components
|
||||
{
|
||||
public partial class TorrentActions : IAsyncDisposable
|
||||
{
|
||||
private const int _defaultVersion = 5;
|
||||
|
||||
private bool _disposedValue;
|
||||
private int? _version;
|
||||
|
||||
private List<UIAction>? _actions;
|
||||
|
||||
@@ -63,7 +60,7 @@ namespace Lantean.QBTMud.Components
|
||||
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public MudDialogInstance? MudDialog { get; set; }
|
||||
public IMudDialogInstance? MudDialog { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public UIAction? ParentAction { get; set; }
|
||||
@@ -74,37 +71,14 @@ namespace Lantean.QBTMud.Components
|
||||
|
||||
protected bool OverlayVisible { get; set; }
|
||||
|
||||
protected int MajorVersion
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_version is not null)
|
||||
{
|
||||
return _version.Value;
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(Version))
|
||||
{
|
||||
return _defaultVersion;
|
||||
}
|
||||
|
||||
if (!System.Version.TryParse(Version.Replace("v", ""), out var version))
|
||||
{
|
||||
return _defaultVersion;
|
||||
}
|
||||
|
||||
_version = version.Major;
|
||||
|
||||
return _version.Value;
|
||||
}
|
||||
}
|
||||
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_actions =
|
||||
[
|
||||
new("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
|
||||
new("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
|
||||
new("pause", "Pause", MajorVersion < 5 ? Icons.Material.Filled.Pause : Icons.Material.Filled.Stop, Color.Warning, CreateCallback(Pause)),
|
||||
new("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
|
||||
new("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
|
||||
new("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
|
||||
@@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components
|
||||
thereAreFirstLastPiecePrio = true;
|
||||
}
|
||||
|
||||
if (torrent.Progress != 1.0) // not downloaded
|
||||
if (torrent.Progress < 0.999999) // not downloaded
|
||||
{
|
||||
allAreDownloaded = false;
|
||||
}
|
||||
|
@@ -8,19 +8,17 @@
|
||||
Class="unselectable"
|
||||
MaxHeight="@MaxHeight"
|
||||
AnchorOrigin="@AnchorOrigin"
|
||||
TransformOrigin="TransformOrigin"
|
||||
RelativeWidth="@FullWidth"
|
||||
TransformOrigin="@TransformOrigin"
|
||||
RelativeWidth="@RelativeWidth"
|
||||
OverflowBehavior="OverflowBehavior.FlipAlways"
|
||||
Style="@_popoverStyle"
|
||||
@ontouchend:preventDefault>
|
||||
<CascadingValue Value="@(FakeMenu)">
|
||||
@if (_showChildren)
|
||||
{
|
||||
<MudList T="object"
|
||||
Class="unselectable"
|
||||
Dense="@Dense">
|
||||
<MudList T="object" Class="unselectable" Dense="@Dense">
|
||||
@ChildContent
|
||||
</MudList>
|
||||
</MudList>
|
||||
}
|
||||
</CascadingValue>
|
||||
</MudPopover>
|
||||
|
@@ -7,12 +7,6 @@ using MudBlazor.Utilities;
|
||||
|
||||
namespace Lantean.QBTMud.Components.UI
|
||||
{
|
||||
// This is a very hacky approach but works for now.
|
||||
// This needs to inherit from MudMenu because MudMenuItem needs a MudMenu passed to it to control the close of the menu when an item is clicked.
|
||||
// MudPopover isn't ideal for this because that is designed to be used relative to an activator which in these cases it isn't.
|
||||
// Ideally this should be changed to use something like the way the DialogService works.
|
||||
|
||||
// Or - rework this to have a hidden MudMenu and hook into the OpenChanged event to monitor when the MudMenuItem closes it.
|
||||
public partial class ContextMenu : MudComponentBase
|
||||
{
|
||||
private bool _open;
|
||||
@@ -61,7 +55,7 @@ namespace Lantean.QBTMud.Components.UI
|
||||
/// </summary>
|
||||
[Parameter]
|
||||
[Category(CategoryTypes.Menu.PopupAppearance)]
|
||||
public bool FullWidth { get; set; }
|
||||
public DropdownWidth RelativeWidth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the max height the menu can have when open.
|
||||
@@ -219,56 +213,58 @@ namespace Lantean.QBTMud.Components.UI
|
||||
}
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
protected override Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (!_isResized)
|
||||
{
|
||||
await DeterminePosition();
|
||||
//await DeterminePosition();
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private async Task DeterminePosition()
|
||||
{
|
||||
var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
|
||||
double? contextMenuHeight = null;
|
||||
double? contextMenuWidth = null;
|
||||
//private async Task DeterminePosition()
|
||||
//{
|
||||
// var mainContentSize = await JSRuntime.GetInnerDimensions(".mud-main-content");
|
||||
// double? contextMenuHeight = null;
|
||||
// double? contextMenuWidth = null;
|
||||
|
||||
var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
|
||||
// var popoverHolder = PopoverService.ActivePopovers.FirstOrDefault(p => p.UserAttributes.ContainsKey("tracker") && (string?)p.UserAttributes["tracker"] == Id);
|
||||
|
||||
var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
|
||||
if (popoverSize.Height > 0)
|
||||
{
|
||||
contextMenuHeight = popoverSize.Height;
|
||||
contextMenuWidth = popoverSize.Width;
|
||||
}
|
||||
else
|
||||
{
|
||||
return;
|
||||
}
|
||||
// var popoverSize = await JSRuntime.GetBoundingClientRect($"#popovercontent-{popoverHolder?.Id}");
|
||||
// if (popoverSize.Height > 0)
|
||||
// {
|
||||
// contextMenuHeight = popoverSize.Height;
|
||||
// contextMenuWidth = popoverSize.Width;
|
||||
// }
|
||||
// else
|
||||
// {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// the bottom position of the popover will be rendered off screen
|
||||
if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
|
||||
{
|
||||
// adjust the top of the context menu
|
||||
var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
|
||||
_y -= overshoot;
|
||||
// // the bottom position of the popover will be rendered off screen
|
||||
// if (_y - _diff + contextMenuHeight.Value >= mainContentSize.Height)
|
||||
// {
|
||||
// // adjust the top of the context menu
|
||||
// var overshoot = Math.Abs(mainContentSize.Height - (_y - _diff + contextMenuHeight.Value));
|
||||
// _y -= overshoot;
|
||||
|
||||
if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
|
||||
{
|
||||
MaxHeight = (int)(mainContentSize.Height - _y + _diff);
|
||||
}
|
||||
}
|
||||
// if (_y - _diff + contextMenuHeight >= mainContentSize.Height)
|
||||
// {
|
||||
// MaxHeight = (int)(mainContentSize.Height - _y + _diff);
|
||||
// }
|
||||
// }
|
||||
|
||||
if (_x + contextMenuWidth.Value > mainContentSize.Width)
|
||||
{
|
||||
var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
|
||||
_x -= overshoot;
|
||||
}
|
||||
// if (_x + contextMenuWidth.Value > mainContentSize.Width)
|
||||
// {
|
||||
// var overshoot = Math.Abs(mainContentSize.Width - (_x + contextMenuWidth.Value));
|
||||
// _x -= overshoot;
|
||||
// }
|
||||
|
||||
SetPopoverStyle(_x, _y);
|
||||
_isResized = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
// SetPopoverStyle(_x, _y);
|
||||
// _isResized = true;
|
||||
// await InvokeAsync(StateHasChanged);
|
||||
//}
|
||||
|
||||
private (double x, double y) GetPositionFromArgs(EventArgs eventArgs)
|
||||
{
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="@Classname">
|
||||
<div @onclick="EventUtil.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
||||
<div @onclick="this.AsNonRenderingEventHandler<MouseEventArgs>(OnClickHandler)" class="@LinkClassname" @onlongpress="OnLongPressInternal" @oncontextmenu="OnContextMenuInternal" @oncontextmenu:preventDefault>
|
||||
@if (!string.IsNullOrEmpty(Icon))
|
||||
{
|
||||
<MudIcon Icon="@Icon" Color="@IconColor" Class="@IconClassname" />
|
||||
|
@@ -4,6 +4,7 @@ using Lantean.QBTMud.Models;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.Components.Web;
|
||||
using MudBlazor;
|
||||
using System;
|
||||
|
||||
namespace Lantean.QBTMud.Components.UI
|
||||
{
|
||||
@@ -13,6 +14,7 @@ namespace Lantean.QBTMud.Components.UI
|
||||
private readonly string _columnSelectionStorageKey = $"DynamicTable{_typeName}.ColumnSelection";
|
||||
private readonly string _columnSortStorageKey = $"DynamicTable{_typeName}.ColumnSort";
|
||||
private readonly string _columnWidthsStorageKey = $"DynamicTable{_typeName}.ColumnWidths";
|
||||
private readonly string _columnOrderStorageKey = $"DynamicTable{_typeName}.ColumnOrder";
|
||||
|
||||
[Inject]
|
||||
public ILocalStorageService LocalStorage { get; set; } = default!;
|
||||
@@ -80,14 +82,24 @@ namespace Lantean.QBTMud.Components.UI
|
||||
|
||||
protected HashSet<string> SelectedColumns { get; set; } = [];
|
||||
|
||||
private static readonly IReadOnlyList<ColumnDefinition<T>> EmptyColumns = Array.Empty<ColumnDefinition<T>>();
|
||||
|
||||
private Dictionary<string, int?> _columnWidths = [];
|
||||
|
||||
private Dictionary<string, int> _columnOrder = [];
|
||||
|
||||
private string? _sortColumn;
|
||||
|
||||
private SortDirection _sortDirection;
|
||||
|
||||
private readonly Dictionary<string, TdExtended> _tds = [];
|
||||
|
||||
private IReadOnlyList<ColumnDefinition<T>> _visibleColumns = EmptyColumns;
|
||||
|
||||
private bool _columnsDirty = true;
|
||||
|
||||
private IEnumerable<ColumnDefinition<T>>? _lastColumnDefinitions;
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
{
|
||||
HashSet<string> selectedColumns;
|
||||
@@ -106,6 +118,13 @@ namespace Lantean.QBTMud.Components.UI
|
||||
SelectedColumns = selectedColumns;
|
||||
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedColumns = selectedColumns;
|
||||
}
|
||||
|
||||
_lastColumnDefinitions = ColumnDefinitions;
|
||||
MarkColumnsDirty();
|
||||
|
||||
string? sortColumn;
|
||||
SortDirection sortDirection;
|
||||
@@ -134,11 +153,24 @@ namespace Lantean.QBTMud.Components.UI
|
||||
await SortDirectionChanged.InvokeAsync(_sortDirection);
|
||||
}
|
||||
|
||||
MarkColumnsDirty();
|
||||
|
||||
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
|
||||
if (storedColumnsWidths is not null)
|
||||
{
|
||||
_columnWidths = storedColumnsWidths;
|
||||
}
|
||||
MarkColumnsDirty();
|
||||
}
|
||||
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
base.OnParametersSet();
|
||||
if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
|
||||
{
|
||||
_lastColumnDefinitions = ColumnDefinitions;
|
||||
MarkColumnsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
private IEnumerable<T>? GetOrderedItems()
|
||||
@@ -162,18 +194,74 @@ namespace Lantean.QBTMud.Components.UI
|
||||
return Items.OrderByDirection(_sortDirection, sortSelector);
|
||||
}
|
||||
|
||||
protected IEnumerable<ColumnDefinition<T>> GetColumns()
|
||||
protected IReadOnlyList<ColumnDefinition<T>> GetColumns()
|
||||
{
|
||||
var filteredColumns = ColumnDefinitions.Where(c => SelectedColumns.Contains(c.Id)).Where(ColumnFilter);
|
||||
foreach (var column in filteredColumns)
|
||||
if (!_columnsDirty)
|
||||
{
|
||||
return _visibleColumns;
|
||||
}
|
||||
|
||||
_visibleColumns = BuildVisibleColumns();
|
||||
_columnsDirty = false;
|
||||
|
||||
return _visibleColumns;
|
||||
}
|
||||
|
||||
private IReadOnlyList<ColumnDefinition<T>> BuildVisibleColumns()
|
||||
{
|
||||
var filteredColumns = ColumnDefinitions
|
||||
.Where(c => SelectedColumns.Contains(c.Id))
|
||||
.Where(ColumnFilter)
|
||||
.ToList();
|
||||
|
||||
if (filteredColumns.Count == 0)
|
||||
{
|
||||
return EmptyColumns;
|
||||
}
|
||||
|
||||
List<ColumnDefinition<T>> orderedColumns;
|
||||
if (_columnOrder.Count == 0)
|
||||
{
|
||||
orderedColumns = filteredColumns;
|
||||
}
|
||||
else
|
||||
{
|
||||
var orderLookup = _columnOrder.OrderBy(entry => entry.Value).ToList();
|
||||
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
|
||||
orderedColumns = new List<ColumnDefinition<T>>(filteredColumns.Count);
|
||||
|
||||
foreach (var (columnId, _) in orderLookup)
|
||||
{
|
||||
if (!columnDictionary.TryGetValue(columnId, out var column))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
orderedColumns.Add(column);
|
||||
}
|
||||
|
||||
if (orderedColumns.Count != filteredColumns.Count)
|
||||
{
|
||||
var existingIds = new HashSet<string>(orderedColumns.Select(c => c.Id));
|
||||
foreach (var column in filteredColumns)
|
||||
{
|
||||
if (existingIds.Add(column.Id))
|
||||
{
|
||||
orderedColumns.Add(column);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var column in orderedColumns)
|
||||
{
|
||||
if (_columnWidths.TryGetValue(column.Id, out var value))
|
||||
{
|
||||
column.Width = value;
|
||||
}
|
||||
|
||||
yield return column;
|
||||
}
|
||||
|
||||
return orderedColumns;
|
||||
}
|
||||
|
||||
private async Task SetSort(string columnId, SortDirection sortDirection)
|
||||
@@ -280,7 +368,7 @@ namespace Lantean.QBTMud.Components.UI
|
||||
|
||||
public async Task ShowColumnOptionsDialog()
|
||||
{
|
||||
var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths);
|
||||
var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths, _columnOrder);
|
||||
|
||||
if (result == default)
|
||||
{
|
||||
@@ -292,18 +380,27 @@ namespace Lantean.QBTMud.Components.UI
|
||||
SelectedColumns = result.SelectedColumns;
|
||||
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
|
||||
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
||||
MarkColumnsDirty();
|
||||
}
|
||||
|
||||
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
|
||||
{
|
||||
_columnWidths = result.ColumnWidths;
|
||||
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
|
||||
MarkColumnsDirty();
|
||||
}
|
||||
|
||||
if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
|
||||
{
|
||||
_columnOrder = result.ColumnOrder;
|
||||
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
|
||||
MarkColumnsDirty();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -349,6 +446,12 @@ namespace Lantean.QBTMud.Components.UI
|
||||
return className;
|
||||
}
|
||||
|
||||
private void MarkColumnsDirty()
|
||||
{
|
||||
_columnsDirty = true;
|
||||
_visibleColumns = EmptyColumns;
|
||||
}
|
||||
|
||||
private sealed record SortData
|
||||
{
|
||||
public SortData(string sortColumn, SortDirection sortDirection)
|
||||
@@ -362,4 +465,4 @@ namespace Lantean.QBTMud.Components.UI
|
||||
public SortDirection SortDirection { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -328,13 +328,14 @@ namespace Lantean.QBTMud.Helpers
|
||||
return tags;
|
||||
}
|
||||
|
||||
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths)
|
||||
public static async Task<(HashSet<string> SelectedColumns, Dictionary<string, int?> ColumnWidths, Dictionary<string, int> ColumnOrder)> ShowColumnsOptionsDialog<T>(this IDialogService dialogService, List<ColumnDefinition<T>> columnDefinitions, HashSet<string> selectedColumns, Dictionary<string, int?> widths, Dictionary<string, int> order)
|
||||
{
|
||||
var parameters = new DialogParameters
|
||||
{
|
||||
{ nameof(ColumnOptionsDialog<T>.Columns), columnDefinitions },
|
||||
{ nameof(ColumnOptionsDialog<T>.SelectedColumns), selectedColumns },
|
||||
{ nameof(ColumnOptionsDialog<T>.Widths), widths },
|
||||
{ nameof(ColumnOptionsDialog<T>.Order), order },
|
||||
};
|
||||
|
||||
var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
|
||||
@@ -344,7 +345,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
return default;
|
||||
}
|
||||
|
||||
return ((HashSet<string>, Dictionary<string, int?>))dialogResult.Data;
|
||||
return ((HashSet<string>, Dictionary<string, int?>, Dictionary<string, int>))dialogResult.Data;
|
||||
}
|
||||
|
||||
public static async Task<bool> ShowConfirmDialog(this IDialogService dialogService, string title, string content)
|
||||
|
@@ -37,7 +37,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
time = TimeSpan.FromSeconds(seconds.Value);
|
||||
}
|
||||
catch (OverflowException)
|
||||
catch
|
||||
{
|
||||
return "∞";
|
||||
}
|
||||
@@ -129,7 +129,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
return "";
|
||||
}
|
||||
|
||||
return Size(size);
|
||||
return Size(size, prefix, suffix);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
34
Lantean.QBTMud/Helpers/VersionHelper.cs
Normal file
34
Lantean.QBTMud/Helpers/VersionHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,24 +1,22 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.BlazorWebAssembly">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CompressionEnabled>false</CompressionEnabled>
|
||||
<LangVersion>12</LangVersion>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<CompressionEnabled>false</CompressionEnabled>
|
||||
<LangVersion>12</LangVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.10" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.10" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.1" />
|
||||
<PackageReference Include="MudBlazor" Version="7.15.0" />
|
||||
<PackageReference Include="MudBlazor.ThemeManager" Version="2.1.0" />
|
||||
<!-- added to fix vuln in dependency -->
|
||||
<PackageReference Include="System.Text.Json" Version="8.0.5" />
|
||||
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
|
||||
<PackageReference Include="ByteSize" Version="2.1.2" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.5" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.5" PrivateAssets="all" />
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.5" />
|
||||
<PackageReference Include="MudBlazor" Version="8.7.0" />
|
||||
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@@ -10,7 +10,8 @@
|
||||
}
|
||||
|
||||
<CascadingValue Value="Torrents">
|
||||
<CascadingValue Value="MainData">
|
||||
<CascadingValue Value="_torrentsVersion" Name="TorrentsVersion">
|
||||
<CascadingValue Value="MainData">
|
||||
<CascadingValue Value="Preferences">
|
||||
<CascadingValue Value="SortColumnChanged" Name="SortColumnChanged">
|
||||
<CascadingValue Value="SortColumn" Name="SortColumn">
|
||||
@@ -36,23 +37,23 @@
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten);">
|
||||
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
|
||||
@if (MainData?.LostConnection == true)
|
||||
{
|
||||
<MudText Class="mx-2 mb-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
|
||||
}
|
||||
<MudSpacer />
|
||||
<MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
|
||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
|
||||
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
|
||||
@{
|
||||
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
||||
}
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
||||
<MudDivider Vertical="true" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
|
||||
<MudText Class="mr-1 mb-1">
|
||||
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")
|
||||
@@ -65,5 +66,6 @@
|
||||
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
|
||||
</MudText>
|
||||
</MudAppBar>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
|
@@ -1,4 +1,6 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Components;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
@@ -52,22 +54,36 @@ namespace Lantean.QBTMud.Layout
|
||||
|
||||
protected string? SearchText { get; set; }
|
||||
|
||||
protected IEnumerable<Torrent> Torrents => GetTorrents();
|
||||
protected IReadOnlyList<Torrent> Torrents => GetTorrents();
|
||||
|
||||
protected bool IsAuthenticated { get; set; }
|
||||
|
||||
protected bool LostConnection { get; set; }
|
||||
|
||||
private List<Torrent> GetTorrents()
|
||||
private IReadOnlyList<Torrent> _visibleTorrents = Array.Empty<Torrent>();
|
||||
|
||||
private bool _torrentsDirty = true;
|
||||
private int _torrentsVersion;
|
||||
|
||||
private IReadOnlyList<Torrent> GetTorrents()
|
||||
{
|
||||
if (!_torrentsDirty)
|
||||
{
|
||||
return _visibleTorrents;
|
||||
}
|
||||
|
||||
if (MainData is null)
|
||||
{
|
||||
return [];
|
||||
_visibleTorrents = Array.Empty<Torrent>();
|
||||
_torrentsDirty = false;
|
||||
return _visibleTorrents;
|
||||
}
|
||||
|
||||
var filterState = new FilterState(Category, Status, Tag, Tracker, MainData.ServerState.UseSubcategories, SearchText);
|
||||
_visibleTorrents = MainData.Torrents.Values.Filter(filterState).ToList();
|
||||
_torrentsDirty = false;
|
||||
|
||||
return MainData.Torrents.Values.Filter(filterState).ToList();
|
||||
return _visibleTorrents;
|
||||
}
|
||||
|
||||
protected override async Task OnInitializedAsync()
|
||||
@@ -83,7 +99,8 @@ namespace Lantean.QBTMud.Layout
|
||||
Preferences = await ApiClient.GetApplicationPreferences();
|
||||
Version = await ApiClient.GetApplicationVersion();
|
||||
var data = await ApiClient.GetMainData(_requestId);
|
||||
MainData = DataManager.CreateMainData(data);
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
MarkTorrentsDirty();
|
||||
|
||||
_requestId = data.ResponseId;
|
||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||
@@ -126,32 +143,51 @@ namespace Lantean.QBTMud.Layout
|
||||
return;
|
||||
}
|
||||
|
||||
var shouldRender = false;
|
||||
|
||||
if (MainData is null || data.FullUpdate)
|
||||
{
|
||||
MainData = DataManager.CreateMainData(data);
|
||||
MainData = DataManager.CreateMainData(data, Version);
|
||||
MarkTorrentsDirty();
|
||||
shouldRender = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
DataManager.MergeMainData(data, MainData);
|
||||
var dataChanged = DataManager.MergeMainData(data, MainData, out var filterChanged);
|
||||
if (filterChanged)
|
||||
{
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
else if (dataChanged)
|
||||
{
|
||||
IncrementTorrentsVersion();
|
||||
}
|
||||
shouldRender = dataChanged;
|
||||
}
|
||||
|
||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||
if (MainData is not null)
|
||||
{
|
||||
_refreshInterval = MainData.ServerState.RefreshInterval;
|
||||
}
|
||||
_requestId = data.ResponseId;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
if (shouldRender)
|
||||
{
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, category => Category = category);
|
||||
protected EventCallback<string> CategoryChanged => EventCallback.Factory.Create<string>(this, OnCategoryChanged);
|
||||
|
||||
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, status => Status = status);
|
||||
protected EventCallback<Status> StatusChanged => EventCallback.Factory.Create<Status>(this, OnStatusChanged);
|
||||
|
||||
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, tag => Tag = tag);
|
||||
protected EventCallback<string> TagChanged => EventCallback.Factory.Create<string>(this, OnTagChanged);
|
||||
|
||||
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, tracker => Tracker = tracker);
|
||||
protected EventCallback<string> TrackerChanged => EventCallback.Factory.Create<string>(this, OnTrackerChanged);
|
||||
|
||||
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, term => SearchText = term);
|
||||
protected EventCallback<string> SearchTermChanged => EventCallback.Factory.Create<string>(this, OnSearchTermChanged);
|
||||
|
||||
protected EventCallback<string> SortColumnChanged => EventCallback.Factory.Create<string>(this, columnId => SortColumn = columnId);
|
||||
|
||||
@@ -167,6 +203,76 @@ namespace Lantean.QBTMud.Layout
|
||||
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
|
||||
}
|
||||
|
||||
private void OnCategoryChanged(string category)
|
||||
{
|
||||
if (Category == category)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Category = category;
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
|
||||
private void OnStatusChanged(Status status)
|
||||
{
|
||||
if (Status == status)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Status = status;
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
|
||||
private void OnTagChanged(string tag)
|
||||
{
|
||||
if (Tag == tag)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tag = tag;
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
|
||||
private void OnTrackerChanged(string tracker)
|
||||
{
|
||||
if (Tracker == tracker)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Tracker = tracker;
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
|
||||
private void OnSearchTermChanged(string term)
|
||||
{
|
||||
if (SearchText == term)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
SearchText = term;
|
||||
MarkTorrentsDirty();
|
||||
}
|
||||
|
||||
private void MarkTorrentsDirty()
|
||||
{
|
||||
_torrentsDirty = true;
|
||||
IncrementTorrentsVersion();
|
||||
}
|
||||
|
||||
private void IncrementTorrentsVersion()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
_torrentsVersion++;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
protected virtual void Dispose(bool disposing)
|
||||
{
|
||||
if (!_disposedValue)
|
||||
@@ -188,4 +294,4 @@ namespace Lantean.QBTMud.Layout
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -20,16 +20,18 @@
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
|
||||
</MudBadge>
|
||||
}
|
||||
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
|
||||
<MudSwitch T="bool" Label="Dark Mode" LabelPlacement="Placement.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
|
||||
<Menu @ref="Menu" />
|
||||
</MudAppBar>
|
||||
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
|
||||
<MudDrawer @bind-Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
|
||||
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
|
||||
</MudDrawer>
|
||||
<CascadingValue Value="Theme">
|
||||
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
|
||||
<CascadingValue Value="Menu">
|
||||
@Body
|
||||
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
|
||||
@Body
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
</CascadingValue>
|
||||
|
@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
|
||||
|
||||
private bool _disposedValue;
|
||||
|
||||
[Inject]
|
||||
protected NavigationManager NavigationManager { get; set; } = default!;
|
||||
|
||||
[Inject]
|
||||
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
|
||||
|
||||
@@ -78,21 +75,21 @@ namespace Lantean.QBTMud.Layout
|
||||
{
|
||||
IsDarkMode = isDarkMode.Value;
|
||||
}
|
||||
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
|
||||
await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
|
||||
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
|
||||
protected Task OnSystemPreferenceChanged(bool value)
|
||||
protected Task OnSystemDarkModeChanged(bool value)
|
||||
{
|
||||
IsDarkMode = value;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
||||
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
|
||||
{
|
||||
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
|
||||
if (browserViewportEventArgs.Breakpoint <= Breakpoint.Sm)
|
||||
{
|
||||
DrawerOpen = false;
|
||||
}
|
||||
@@ -101,7 +98,17 @@ namespace Lantean.QBTMud.Layout
|
||||
DrawerOpen = true;
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
if (ErrorBoundary?.Errors.Count > 0)
|
||||
{
|
||||
ErrorDrawerOpen = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
await Task.Delay(250);
|
||||
ErrorDrawerOpen = false;
|
||||
}
|
||||
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
protected void ToggleErrorDrawer()
|
||||
|
@@ -11,7 +11,8 @@
|
||||
Dictionary<string, HashSet<string>> tagState,
|
||||
Dictionary<string, HashSet<string>> categoriesState,
|
||||
Dictionary<string, HashSet<string>> statusState,
|
||||
Dictionary<string, HashSet<string>> trackersState)
|
||||
Dictionary<string, HashSet<string>> trackersState,
|
||||
int majorVersion)
|
||||
{
|
||||
Torrents = torrents.ToDictionary();
|
||||
Tags = tags.ToHashSet();
|
||||
@@ -22,6 +23,7 @@
|
||||
CategoriesState = categoriesState;
|
||||
StatusState = statusState;
|
||||
TrackersState = trackersState;
|
||||
MajorVersion = majorVersion;
|
||||
}
|
||||
|
||||
public Dictionary<string, Torrent> Torrents { get; }
|
||||
@@ -36,5 +38,6 @@
|
||||
public Dictionary<string, HashSet<string>> TrackersState { get; }
|
||||
public string? SelectedTorrentHash { get; set; }
|
||||
public bool LostConnection { get; set; }
|
||||
public int MajorVersion { get; }
|
||||
}
|
||||
}
|
@@ -8,6 +8,7 @@
|
||||
Completed,
|
||||
Resumed,
|
||||
Paused,
|
||||
Stopped,
|
||||
Active,
|
||||
Inactive,
|
||||
Stalled,
|
||||
@@ -15,6 +16,6 @@
|
||||
StalledDownloading,
|
||||
Checking,
|
||||
Errored,
|
||||
Stopped
|
||||
|
||||
}
|
||||
}
|
@@ -12,89 +12,96 @@
|
||||
|
||||
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
|
||||
<MudTabPanel Text="About">
|
||||
<div class="d-flex gap-4">
|
||||
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop" Height="162" Width="94" />
|
||||
<MudGrid Class="mx-0 mt-0 mb-3">
|
||||
<MudItem xs="12">
|
||||
<div class="d-flex gap-3">
|
||||
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None" Alt="QBT" Height="32" Width="32" /><MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
||||
<MudGrid Class="mt-0 mb-4">
|
||||
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
|
||||
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
|
||||
Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop"
|
||||
Height="162" Width="94" />
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12" sm="9" md="10" lg="10" xl="11">
|
||||
<div class="d-flex flex-column gap-2">
|
||||
<div class="d-flex gap-3 align-items-center">
|
||||
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None"
|
||||
Alt="QBT" Height="32" Width="32" />
|
||||
<MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
|
||||
</div>
|
||||
|
||||
<MudText Typo="Typo.body1">
|
||||
An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.
|
||||
</MudText>
|
||||
|
||||
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
|
||||
|
||||
<div class="d-flex flex-wrap">
|
||||
<MudText Typo="Typo.body1" Class="fw-bold">Home Page: </MudText>
|
||||
<MudLink Href="https://www.qbittorrent.org" Target="_blank" Class="ms-2">
|
||||
qbittorrent.org
|
||||
</MudLink>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap">
|
||||
<MudText Typo="Typo.body1" Class="fw-bold">Bug Tracker: </MudText>
|
||||
<MudLink Href="https://bugs.qbittorrent.org" Target="_blank" Class="ms-2">
|
||||
bugs.qbittorrent.org
|
||||
</MudLink>
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-wrap">
|
||||
<MudText Typo="Typo.body1" Class="fw-bold">Forum: </MudText>
|
||||
<MudLink Href="https://forum.qbittorrent.org" Target="_blank" Class="ms-2">
|
||||
forum.qbittorrent.org
|
||||
</MudLink>
|
||||
</div>
|
||||
</div>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="12">
|
||||
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Home Page</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudLink Href="https://www.qbittorrent.org" Target="https://www.qbittorrent.org">https://www.qbittorrent.org</MudLink>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Bug Tracker</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudLink Href="https://bugs.qbittorrent.org" Target="https://bugs.qbittorrent.org">https://bugs.qbittorrent.org</MudLink>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Forum</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudLink Href="https://forum.qbittorrent.org" Target="https://forum.qbittorrent.org">https://forum.qbittorrent.org</MudLink>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
</div>
|
||||
</MudContainer>
|
||||
</MudTabPanel>
|
||||
<MudTabPanel Text="Authors">
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
|
||||
<MudText Typo="Typo.body1" Class="py-1">Current maintainer</MudText>
|
||||
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
|
||||
|
||||
<MudGrid Class="mt-0 mb-4">
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Name</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">Name</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudText Typo="Typo.body1">Sledgehammer999</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Nationality</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">Nationality</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudText Typo="Typo.body1">Greece</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">E-mail</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">E-mail</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudText Typo="Typo.body1" Class="py-1">Original author</MudText>
|
||||
<MudText Typo="Typo.h5" Class="py-1">Original author</MudText>
|
||||
<MudGrid Class="mt-0 mb-4">
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Name</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">Name</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudText Typo="Typo.body1">Christophe Dumez</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">Nationality</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">Nationality</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudText Typo="Typo.body1">France</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="2">
|
||||
<MudText Typo="Typo.body1">E-mail</MudText>
|
||||
<MudItem xs="12" md="2">
|
||||
<MudText Typo="Typo.h6">E-mail</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="12" md="10">
|
||||
<MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
@@ -118,7 +125,7 @@
|
||||
(the list might not be up to date)
|
||||
</MudText>
|
||||
<MudList T="string" ReadOnly>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Info"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Armenian:</u> Hrant Ohanyan (hrantohanyan@mail.am)</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
|
||||
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@gmail.com)</MudListItem>
|
||||
@@ -1058,38 +1065,38 @@
|
||||
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
|
||||
|
||||
<MudGrid Class="mt-1 mb-4">
|
||||
<MudItem xs="2">
|
||||
<MudItem xs="3">
|
||||
<MudText Typo="Typo.body1">Qt</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="9">
|
||||
<MudText Typo="Typo.body1">@QtVersion</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudItem xs="3">
|
||||
<MudText Typo="Typo.body1">Libtorrent</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="9">
|
||||
<MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudItem xs="3">
|
||||
<MudText Typo="Typo.body1">Boost</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="9">
|
||||
<MudText Typo="Typo.body1">@BoostVersion</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudItem xs="3">
|
||||
<MudText Typo="Typo.body1">OpenSSL</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="9">
|
||||
<MudText Typo="Typo.body1">@OpensslVersion</MudText>
|
||||
</MudItem>
|
||||
|
||||
<MudItem xs="2">
|
||||
<MudItem xs="3">
|
||||
<MudText Typo="Typo.body1">zlib</MudText>
|
||||
</MudItem>
|
||||
<MudItem xs="10">
|
||||
<MudItem xs="9">
|
||||
<MudText Typo="Typo.body1">@ZlibVersion</MudText>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
@@ -1,6 +1,7 @@
|
||||
@page "/details/{hash}"
|
||||
@layout DetailsLayout
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
@if (!DrawerOpen)
|
||||
{
|
||||
@@ -14,6 +15,7 @@
|
||||
<MudDivider Vertical="true" />
|
||||
<MudText Class="pl-5 no-wrap">@Name</MudText>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
@if (ShowTabs)
|
||||
{
|
||||
|
@@ -49,7 +49,7 @@ namespace Lantean.QBTMud.Pages
|
||||
|
||||
protected override Task OnInitializedAsync()
|
||||
{
|
||||
return DoLogin("admin", "eBGJzbjkJ");
|
||||
return DoLogin("admin", "5FUM5pATq");
|
||||
}
|
||||
|
||||
#endif
|
||||
|
@@ -18,7 +18,7 @@
|
||||
<MudItem xs="12" md="4">
|
||||
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
|
||||
</MudItem>
|
||||
<MudItem xs="2" md="3">
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
|
||||
@foreach (var (value, name) in Categories)
|
||||
{
|
||||
@@ -30,17 +30,21 @@
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="2" md="3">
|
||||
<MudItem xs="12" md="3">
|
||||
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="@("all")">All</MudSelectItem>
|
||||
<MudDivider />
|
||||
@if (Plugins.Count > 0)
|
||||
{
|
||||
<MudDivider />
|
||||
|
||||
}
|
||||
@foreach (var (value, name) in Plugins)
|
||||
{
|
||||
<MudSelectItem Value="value">@name</MudSelectItem>
|
||||
}
|
||||
</MudSelect>
|
||||
</MudItem>
|
||||
<MudItem xs="2" md="2">
|
||||
<MudItem xs="12" md="2">
|
||||
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
|
||||
</MudItem>
|
||||
|
||||
|
@@ -1,12 +1,13 @@
|
||||
@page "/"
|
||||
@layout ListLayout
|
||||
|
||||
<ContextMenu @ref="ContextMenu" Dense="true" AdjustmentX="@(DrawerOpen ? -235 : 0)">
|
||||
<ContextMenu @ref="ContextMenu" Dense="true" RelativeWidth="DropdownWidth.Ignore" AdjustmentX="-242" AdjustmentY="0">
|
||||
<MudMenuItem Icon="@Icons.Material.Outlined.Info" IconColor="Color.Inherit" OnClick="ShowTorrentContextMenu">View torrent details</MudMenuItem>
|
||||
<MudDivider />
|
||||
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
|
||||
</ContextMenu>
|
||||
|
||||
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
|
||||
<MudToolBar Gutters="false" Dense="true">
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
|
||||
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
|
||||
@@ -18,6 +19,7 @@
|
||||
<MudSpacer />
|
||||
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
|
||||
</MudToolBar>
|
||||
</div>
|
||||
|
||||
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
|
||||
<DynamicTable
|
||||
|
@@ -1,4 +1,4 @@
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBitTorrentClient;
|
||||
using Lantean.QBTMud.Components.UI;
|
||||
using Lantean.QBTMud.Helpers;
|
||||
using Lantean.QBTMud.Models;
|
||||
@@ -35,11 +35,17 @@ namespace Lantean.QBTMud.Pages
|
||||
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public IEnumerable<Torrent>? Torrents { get; set; }
|
||||
public IReadOnlyList<Torrent>? Torrents { get; set; }
|
||||
|
||||
[CascadingParameter]
|
||||
public MainData MainData { get; set; } = default!;
|
||||
|
||||
[CascadingParameter(Name = "LostConnection")]
|
||||
public bool LostConnection { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "TorrentsVersion")]
|
||||
public int TorrentsVersion { get; set; }
|
||||
|
||||
[CascadingParameter(Name = "SearchTermChanged")]
|
||||
public EventCallback<string> SearchTermChanged { get; set; }
|
||||
|
||||
@@ -56,7 +62,7 @@ namespace Lantean.QBTMud.Pages
|
||||
|
||||
protected HashSet<Torrent> SelectedItems { get; set; } = [];
|
||||
|
||||
protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0;
|
||||
protected bool ToolbarButtonsEnabled => _toolbarButtonsEnabled;
|
||||
|
||||
protected DynamicTable<Torrent>? Table { get; set; }
|
||||
|
||||
@@ -64,6 +70,15 @@ namespace Lantean.QBTMud.Pages
|
||||
|
||||
protected ContextMenu? ContextMenu { get; set; }
|
||||
|
||||
private object? _lastRenderedTorrents;
|
||||
private QBitTorrentClient.Models.Preferences? _lastPreferences;
|
||||
private bool _lastLostConnection;
|
||||
private bool _hasRendered;
|
||||
private int _lastSelectionCount;
|
||||
private int _lastTorrentsVersion = -1;
|
||||
private bool _pendingSelectionChange;
|
||||
|
||||
private bool _toolbarButtonsEnabled;
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
if (firstRender)
|
||||
@@ -73,9 +88,81 @@ namespace Lantean.QBTMud.Pages
|
||||
}
|
||||
}
|
||||
|
||||
protected override bool ShouldRender()
|
||||
{
|
||||
if (!_hasRendered)
|
||||
{
|
||||
_hasRendered = true;
|
||||
_lastRenderedTorrents = Torrents;
|
||||
_lastPreferences = Preferences;
|
||||
_lastLostConnection = LostConnection;
|
||||
_lastTorrentsVersion = TorrentsVersion;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_pendingSelectionChange)
|
||||
{
|
||||
_pendingSelectionChange = false;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_lastTorrentsVersion != TorrentsVersion)
|
||||
{
|
||||
_lastTorrentsVersion = TorrentsVersion;
|
||||
_lastRenderedTorrents = Torrents;
|
||||
_lastPreferences = Preferences;
|
||||
_lastLostConnection = LostConnection;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(_lastRenderedTorrents, Torrents))
|
||||
{
|
||||
_lastRenderedTorrents = Torrents;
|
||||
_lastPreferences = Preferences;
|
||||
_lastLostConnection = LostConnection;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ReferenceEquals(_lastPreferences, Preferences))
|
||||
{
|
||||
_lastPreferences = Preferences;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_lastLostConnection != LostConnection)
|
||||
{
|
||||
_lastLostConnection = LostConnection;
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_lastSelectionCount != SelectedItems.Count)
|
||||
{
|
||||
_lastSelectionCount = SelectedItems.Count;
|
||||
_toolbarButtonsEnabled = _lastSelectionCount > 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
protected void SelectedItemsChanged(HashSet<Torrent> selectedItems)
|
||||
{
|
||||
SelectedItems = selectedItems;
|
||||
_toolbarButtonsEnabled = SelectedItems.Count > 0;
|
||||
_pendingSelectionChange = true;
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
protected async Task SortDirectionChangedHandler(SortDirection sortDirection)
|
||||
@@ -193,7 +280,7 @@ namespace Lantean.QBTMud.Pages
|
||||
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
|
||||
[
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("#", t => t.Priority),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition("Icon", t => t.State, IconColumn, iconOnly: true, width: 25, tdClass: "table-icon"),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Name", t => t.Name, width: 400),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
|
||||
ColumnDefinitionHelper.CreateColumnDefinition<Torrent>("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),
|
||||
@@ -248,4 +335,5 @@ namespace Lantean.QBTMud.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -4,11 +4,11 @@ namespace Lantean.QBTMud.Services
|
||||
{
|
||||
public interface IDataManager
|
||||
{
|
||||
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
|
||||
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
|
||||
|
||||
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);
|
||||
|
||||
void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList);
|
||||
bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged);
|
||||
|
||||
PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers);
|
||||
|
||||
@@ -16,10 +16,10 @@ namespace Lantean.QBTMud.Services
|
||||
|
||||
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files);
|
||||
|
||||
void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
||||
bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents);
|
||||
|
||||
QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);
|
||||
|
||||
RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -155,7 +155,7 @@ code {
|
||||
}
|
||||
|
||||
.torrent-list .mud-table-container {
|
||||
height: calc(100vh - 149px);
|
||||
height: calc(100vh - 160px);
|
||||
}
|
||||
|
||||
.file-list .mud-table-container {
|
||||
@@ -240,4 +240,19 @@ td .folder-button {
|
||||
|
||||
.mud-dialog .mud-dialog-content {
|
||||
padding-top: 4px !important;
|
||||
}
|
||||
|
||||
.icon-menu-dense {
|
||||
padding-top: 2px;
|
||||
padding-bottom: 2px;
|
||||
}
|
||||
|
||||
.table-icon {
|
||||
width: 25px;
|
||||
max-width: 25px;
|
||||
padding: 0 8px !important;
|
||||
}
|
||||
|
||||
.mud-popover .mud-divider:last-child {
|
||||
display: none;
|
||||
}
|
@@ -9,12 +9,12 @@
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="css/app.css" />
|
||||
<link href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<link rel="stylesheet" href="./css/app.css" />
|
||||
<link rel="icon" type="image/png" href="images/qbittorrent32.png" />
|
||||
<link rel="icon" href="images/qbittorrent-tray.svg">
|
||||
<link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000">
|
||||
<link rel="apple-touch-icon" href="images/qbittorrent32.png">
|
||||
<link rel="icon" href="./images/qbittorrent-tray.svg">
|
||||
<link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
|
||||
<link rel="apple-touch-icon" href="./images/qbittorrent32.png">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
@@ -31,10 +31,10 @@
|
||||
<a href="" class="reload">Reload</a>
|
||||
<a class="dismiss">🗙</a>
|
||||
</div>
|
||||
<script src="_framework/blazor.webassembly.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="js/piecesbar.js"></script>
|
||||
<script src="js/interop.js"></script>
|
||||
<script src="./_framework/blazor.webassembly.js"></script>
|
||||
<script src="./_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
<script src="./js/piecesbar.js"></script>
|
||||
<script src="./js/interop.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
@@ -1,10 +1,10 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
|
@@ -112,7 +112,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
int maxConnecPerTorrent,
|
||||
int maxInactiveSeedingTime,
|
||||
bool maxInactiveSeedingTimeEnabled,
|
||||
int maxRatio,
|
||||
float maxRatio,
|
||||
int maxRatioAct,
|
||||
bool maxRatioEnabled,
|
||||
int maxSeedingTime,
|
||||
@@ -745,7 +745,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
public bool MaxInactiveSeedingTimeEnabled { get; }
|
||||
|
||||
[JsonPropertyName("max_ratio")]
|
||||
public int MaxRatio { get; }
|
||||
public float MaxRatio { get; }
|
||||
|
||||
[JsonPropertyName("max_ratio_act")]
|
||||
public int MaxRatioAct { get; }
|
||||
|
@@ -323,7 +323,7 @@ namespace Lantean.QBitTorrentClient.Models
|
||||
public bool? MaxInactiveSeedingTimeEnabled { get; set; }
|
||||
|
||||
[JsonPropertyName("max_ratio")]
|
||||
public int? MaxRatio { get; set; }
|
||||
public float? MaxRatio { get; set; }
|
||||
|
||||
[JsonPropertyName("max_ratio_act")]
|
||||
public int? MaxRatioAct { get; set; }
|
||||
|
5
global.json
Normal file
5
global.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "9.0.306"
|
||||
}
|
||||
}
|
11
nuget.config
Normal file
11
nuget.config
Normal 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>
|
94
readme.md
94
readme.md
@@ -1,14 +1,84 @@
|
||||
# qbt-mud
|
||||
# qbtmud
|
||||
|
||||
## To-Do
|
||||
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
|
||||
|
||||
- Rename multiple files dialog
|
||||
- ~~RSS feeds and dialogs~~
|
||||
- ~~About~~
|
||||
- ~~Context menu for files list/trackers list/peers list~~
|
||||
- ~~Tag management page~~
|
||||
- ~~Category management page~~
|
||||
- ~~Update all tables to use DynamicTable~~
|
||||
- ~~Log~~
|
||||
- ~~Blocks~~
|
||||
- ~~Search~~
|
||||
## Features
|
||||
|
||||
qbtmud replicates all core features of the qBittorrent WebUI, including:
|
||||
|
||||
- **Torrent Management** – Add, remove, and control torrents.
|
||||
- **Tracker Control** – View and manage trackers.
|
||||
- **Peer Management** – Monitor and manage peers connected to torrents.
|
||||
- **File Prioritization** – Select and prioritize specific files within a torrent.
|
||||
- **Speed Limits** – Set global and per-torrent speed limits.
|
||||
- **RSS Integration** – Subscribe to RSS feeds for automated torrent downloads.
|
||||
- **Search Functionality** – Integrated torrent search.
|
||||
- **Sequential Downloading** – Download files in order for media streaming.
|
||||
- **Super Seeding Mode** – Efficiently distribute torrents as an initial seeder.
|
||||
- **IP Filtering** – Improve security by filtering specific IP addresses.
|
||||
- **IPv6 Support** – Full support for IPv6 networks.
|
||||
- **Bandwidth Scheduler** – Schedule bandwidth limits.
|
||||
- **WebUI Access** – Remotely manage torrents through the WebUI.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
|
||||
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.
|
||||
|
Reference in New Issue
Block a user