Code cleanup, bugfixes and rework of torrent actions submenus for touch devices.

This commit is contained in:
ahjephson
2024-05-21 15:38:49 +01:00
parent 79847a6ad8
commit 8cbdf4f2b6
49 changed files with 677 additions and 747 deletions

View File

@@ -22,6 +22,7 @@
<ItemGroup>
<ProjectReference Include="..\Lantean.QBitTorrentClient\Lantean.QBitTorrentClient.csproj" />
<ProjectReference Include="..\Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,7 +2,7 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent">
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent" MaximumFileCount="50" >
<ButtonTemplate>
<MudButton HtmlTag="label"
Variant="Variant.Filled"

View File

@@ -2,7 +2,6 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Linq.Expressions;
using System.Reflection;
namespace Lantean.QBTMudBlade.Components.Dialogs

View File

@@ -1,5 +1,4 @@
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;

View File

@@ -4,7 +4,7 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudTextField Label="@Label" Value="@Value" />
<MudTextField T="T" Label="@Label" Value="@Value" />
</MudItem>
</MudGrid>
</DialogContent>

View File

@@ -4,10 +4,10 @@
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudNumericField Label="@Label" Value="@Value" Min="Min" Max="Max" />
<MudNumericField T="T" Label="@Label" Value="@Value" Min="@Min" Max="@Max" />
</MudItem>
<MudItem xs="12">
<MudSlider ValueLabel="true" Value="@Value" Min="Min" Max="Max" />
<MudSlider T="T" ValueLabel="true" Value="@Value" Min="@Min" Max="@Max" />
</MudItem>
</MudGrid>
</DialogContent>

View File

@@ -0,0 +1,9 @@
<MudDialog ContentStyle="mix-width: 400px">
<DialogContent>
<MudGrid>
<MudItem xl="12">
<TorrentActions Hashes="Hashes" ParentAction="ParentAction" RenderType="RenderType.Children" AfterAction="CloseDialog" />
</MudItem>
</MudGrid>
</DialogContent>
</MudDialog>

View File

@@ -0,0 +1,29 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class SubMenuDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public TorrentAction? ParentAction { get; set; }
[Parameter]
public IEnumerable<string> Hashes { get; set; } = [];
protected Task CloseDialog()
{
MudDialog.Close();
return Task.CompletedTask;
}
protected void Cancel()
{
MudDialog.Cancel();
}
}
}

View File

@@ -11,19 +11,20 @@
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Loading="@(Items is null)"
SelectOnRowClick="false"
Striped="Striped"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false"
Loading="@(Items is null)"
MultiSelection="MultiSelection"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChangedInternal"
HorizontalScrollbar="true"
OnRowClick="OnRowClickInternal"
RowStyleFunc="RowStyleFuncInternal"
Virtualize="true"
AllowUnsorted="false"
Class="@Class">
<ColGroup>
<col style="width: 30px" />
@@ -35,10 +36,11 @@
<HeaderContent>
@foreach (var column in GetColumns())
{
<MudTh Class="overflow-cell" Style="@(GetColumnStyle(column))">
var className = column.IconOnly ? null : "overflow-cell";
<MudTh Class="@className" Style="@(GetColumnStyle(column))">
@if (column.SortSelector is not null)
{
<MudTableSortLabel T="T" SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@column.Header</MudTableSortLabel>
<SortLabel SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@column.Header</SortLabel>
}
else
{

View File

@@ -59,7 +59,7 @@ namespace Lantean.QBTMudBlade.Components
public T? SelectedItem { get; set; }
[Parameter]
public Func<ColumnDefinition<T>, bool> ColumnFilter { get; set; } = (t => true);
public Func<ColumnDefinition<T>, bool> ColumnFilter { get; set; } = t => true;
[Parameter]
public EventCallback<string> SortColumnChanged { get; set; }
@@ -104,11 +104,11 @@ namespace Lantean.QBTMudBlade.Components
string? sortColumn;
SortDirection sortDirection;
var storedColumnSort = await LocalStorage.GetItemAsync<Tuple<string, SortDirection>>(_columnSortStorageKey);
if (storedColumnSort is not null)
var sortData = await LocalStorage.GetItemAsync<SortData>(_columnSortStorageKey);
if (sortData is not null)
{
sortColumn = storedColumnSort.Item1;
sortDirection = storedColumnSort.Item2;
sortColumn = sortData.SortColumn;
sortDirection = sortData.SortDirection;
}
else
{
@@ -172,6 +172,12 @@ namespace Lantean.QBTMudBlade.Components
private async Task SetSort(string columnId, SortDirection sortDirection)
{
if (sortDirection == SortDirection.None)
{
return;
}
await LocalStorage.SetItemAsync(_columnSortStorageKey, new SortData(columnId, sortDirection));
if (_sortColumn != columnId)
{
_sortColumn = columnId;
@@ -183,8 +189,6 @@ namespace Lantean.QBTMudBlade.Components
_sortDirection = sortDirection;
await SortDirectionChanged.InvokeAsync(_sortDirection);
}
await LocalStorage.SetItemAsync(_columnSortStorageKey, new Tuple<string, SortDirection>(columnId, sortDirection));
}
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
@@ -200,7 +204,7 @@ namespace Lantean.QBTMudBlade.Components
var style = "user-select: none; cursor: pointer;";
if (EqualityComparer<T>.Default.Equals(item, SelectedItem))
{
style += " background-color: var(--mud-palette-dark-darken)";
style += " background-color: var(--mud-palette-grey-dark); color: var(--mud-palette-grey-light) !important;";
}
return style;
}
@@ -208,6 +212,7 @@ namespace Lantean.QBTMudBlade.Components
protected async Task SelectedItemsChangedInternal(HashSet<T> selectedItems)
{
await SelectedItemsChanged.InvokeAsync(selectedItems);
SelectedItems = selectedItems;
}
public async Task ShowColumnOptionsDialog()
@@ -222,8 +227,8 @@ namespace Lantean.QBTMudBlade.Components
if (!SelectedColumns.SetEquals(result.SelectedColumns))
{
SelectedColumns = result.SelectedColumns;
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
}
if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
@@ -275,5 +280,18 @@ namespace Lantean.QBTMudBlade.Components
return className;
}
private sealed record SortData
{
public SortData(string sortColumn, SortDirection sortDirection)
{
SortColumn = sortColumn;
SortDirection = sortDirection;
}
public string SortColumn { get; init; }
public SortDirection SortDirection { get; init; }
}
}
}

View File

@@ -2,8 +2,8 @@
<MudListItem OnClick="ClearErrors">Clear Errors</MudListItem>
<MudListItem OnClick="ClearErrorsAndResumeAsync">Clear Errors and Resume</MudListItem>
<MudDivider />
@foreach (var error in Errors)
{
<MudListItem OnClick="@(e => ShowException(error))">@error.Message</MudListItem>
}
@foreach (var error in Errors)
{
<MudListItem OnClick="@(e => ShowException(error))">@error.Message</MudListItem>
}
</MudList>

View File

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

View File

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

View File

@@ -24,10 +24,8 @@
T="ContentItem"
ColumnDefinitions="Columns"
Items="Files"
MultiSelection="true"
MultiSelection="false"
PreSorted="true"
SelectedItems="SelectedItems"
SelectedItemsChanged="SelectedItemsChanged"
SelectedItemChanged="SelectedItemChanged"
SortColumnChanged="SortColumnChanged"
SortDirectionChanged="SortDirectionChanged"
@@ -43,7 +41,7 @@
<div style="@($"margin-left: {context.Data.Level * 14}px")">
@if (context.Data.IsFolder)
{
<MudIconButton Edge="Edge.Start" ButtonType="ButtonType.Button" Icon="@(ExpandedNodes.Contains(context.Data.Name) ? Icons.Material.Filled.KeyboardArrowDown : Icons.Material.Filled.KeyboardArrowRight)" OnClick="@(c => ToggleNode(context.Data, c))"></MudIconButton>
<MudIconButton Edge="Edge.Start" ButtonType="ButtonType.Button" Icon="@(ExpandedNodes.Contains(context.Data.Name) ? Icons.Material.Filled.KeyboardArrowDown : Icons.Material.Filled.KeyboardArrowRight)" OnClick="@(c => ToggleNode(context.Data))"></MudIconButton>
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-0" Style="margin-right: 4px; position: relative; top: 7px; margin-left: -15px" />
}
@context.Data.DisplayName

View File

@@ -5,18 +5,16 @@ using Lantean.QBTMudBlade.Filter;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using System.Collections.ObjectModel;
using System.Net;
using System.Linq;
namespace Lantean.QBTMudBlade.Components
{
public partial class FilesTab : IAsyncDisposable
{
private readonly bool _refreshEnabled = true;
private const string _expandedNodesStorageKey = "FilesTab.ExpandedNodes";
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
@@ -36,6 +34,9 @@ namespace Lantean.QBTMudBlade.Components
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[Inject]
protected ILocalStorageService LocalStorage { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
@@ -45,8 +46,6 @@ namespace Lantean.QBTMudBlade.Components
protected IEnumerable<ContentItem> Files => GetFiles();
protected HashSet<ContentItem> SelectedItems { get; set; } = [];
private List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
protected ContentItem? SelectedItem { get; set; }
@@ -59,6 +58,7 @@ namespace Lantean.QBTMudBlade.Components
private DynamicTable<ContentItem>? Table { get; set; }
private string? _previousHash;
private string? _sortColumn;
private SortDirection _sortDirection;
@@ -110,15 +110,9 @@ namespace Lantean.QBTMudBlade.Components
Filters = filters;
}
protected async Task RemoveFilter()
protected void RemoveFilter()
{
Filters = null;
await InvokeAsync(StateHasChanged);
if (FileList is null)
{
return;
}
SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
public async ValueTask DisposeAsync()
@@ -160,15 +154,9 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected async Task SearchTextChanged(string value)
protected void SearchTextChanged(string value)
{
SearchText = value;
await InvokeAsync(StateHasChanged);
if (FileList is null)
{
return;
}
SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
protected async Task EnabledValueChanged(ContentItem contentItem, bool value)
@@ -237,10 +225,25 @@ namespace Lantean.QBTMudBlade.Components
return;
}
if (Hash == _previousHash)
{
return;
}
_previousHash = Hash;
var contents = await ApiClient.GetTorrentContents(Hash);
FileList = DataManager.CreateContentsList(contents);
SelectedItems = FileList.Values.Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
if (expandedNodes is not null)
{
ExpandedNodes = expandedNodes;
}
else
{
ExpandedNodes.Clear();
}
}
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
@@ -253,7 +256,7 @@ namespace Lantean.QBTMudBlade.Components
IEnumerable<int> fileIndexes;
if (contentItem.IsFolder)
{
fileIndexes = GetChildren(contentItem).Where(c => !c.IsFolder).Select(c => c.Index);
fileIndexes = GetDescendants(contentItem).Where(c => !c.IsFolder).Select(c => c.Index);
}
else
{
@@ -269,11 +272,13 @@ namespace Lantean.QBTMudBlade.Components
{
return;
}
var contentItem = FileList.Values.FirstOrDefault(c => c.Index == SelectedItem.Index);
if (contentItem is null)
{
return;
}
var name = contentItem.GetFileName();
await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async v => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + v));
}
@@ -293,44 +298,7 @@ namespace Lantean.QBTMudBlade.Components
SelectedItem = item;
}
protected async Task SelectedItemsChanged(HashSet<ContentItem> selectedItems)
{
if (Hash is null || Files is null)
{
return;
}
var unselectedItems = Files.Except(SelectedItems);
if (unselectedItems.Any())
{
await ApiClient.SetFilePriority(Hash, unselectedItems.Select(c => c.Index), QBitTorrentClient.Models.Priority.DoNotDownload);
foreach (var item in unselectedItems)
{
Files.First(f => f == item).Priority = Priority.DoNotDownload;
}
await InvokeAsync(StateHasChanged);
}
var existingDoNotDownloads = Files.Where(f => f.Priority == Priority.DoNotDownload);
var newlySelectedFiles = selectedItems.Where(f => existingDoNotDownloads.Contains(f));
if (newlySelectedFiles.Any())
{
await ApiClient.SetFilePriority(Hash, newlySelectedFiles.Select(c => c.Index), QBitTorrentClient.Models.Priority.Normal);
foreach (var item in newlySelectedFiles)
{
Files.First(f => f == item).Priority = Priority.Normal;
}
await InvokeAsync(StateHasChanged);
}
}
protected void ToggleNode(ContentItem contentItem, MouseEventArgs args)
protected async Task ToggleNode(ContentItem contentItem)
{
if (ExpandedNodes.Contains(contentItem.Name))
{
@@ -341,10 +309,7 @@ namespace Lantean.QBTMudBlade.Components
ExpandedNodes.Add(contentItem.Name);
}
if (FileList is not null)
{
SelectedItems = GetFiles().Where(f => f.Priority != Priority.DoNotDownload).ToHashSet();
}
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
}
private static QBitTorrentClient.Models.Priority MapPriority(Priority priority)
@@ -359,15 +324,15 @@ namespace Lantean.QBTMudBlade.Components
return sortSelector ?? (i => i.Name);
}
private IEnumerable<ContentItem> GetChildren(ContentItem contentItem)
private IEnumerable<ContentItem> GetDescendants(ContentItem contentItem)
{
if (!contentItem.IsFolder || Files is null)
{
return [];
}
return Files.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
}
return FileList!.Values.Where(f => f.Name.StartsWith(contentItem.Name + Extensions.DirectorySeparator) && !f.IsFolder);
}
private IEnumerable<ContentItem> GetChildren(ContentItem folder, int level)
{
@@ -545,7 +510,7 @@ namespace Lantean.QBTMudBlade.Components
public static List<ColumnDefinition<ContentItem>> ColumnsDefinitions { get; } =
[
CreateColumnDefinition("Name", c => c.Name, width: 200, initialDirection: SortDirection.Ascending, classFunc: c => c.IsFolder ? "pa-0" : "pa-3"),
CreateColumnDefinition("Name", c => c.Name, width: 400, initialDirection: SortDirection.Ascending, classFunc: c => c.IsFolder ? "pa-0" : "pa-3"),
CreateColumnDefinition("Total Size", c => c.Size, c => DisplayHelpers.Size(c.Size)),
CreateColumnDefinition("Progress", c => c.Progress, ProgressBarColumn, tdClass: "table-progress pl-2 pr-2"),
CreateColumnDefinition("Priority", c => c.Priority, tdClass: "table-select pa-0"),

View File

@@ -95,7 +95,7 @@ namespace Lantean.QBTMudBlade.Components.Options
CurrentNetworkInterface = Preferences.CurrentNetworkInterface;
CurrentInterfaceAddress = Preferences.CurrentInterfaceAddress;
SaveResumeDataInterval = Preferences.SaveResumeDataInterval;
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit;
TorrentFileSizeLimit = Preferences.TorrentFileSizeLimit / 1024 / 1024;
RecheckCompletedTorrents = Preferences.RecheckCompletedTorrents;
AppInstanceName = Preferences.AppInstanceName;
RefreshInterval = Preferences.RefreshInterval;
@@ -109,7 +109,7 @@ namespace Lantean.QBTMudBlade.Components.Options
CheckingMemoryUse = Preferences.CheckingMemoryUse;
DiskCache = Preferences.DiskCache;
DiskCacheTtl = Preferences.DiskCacheTtl;
DiskQueueSize = Preferences.DiskQueueSize;
DiskQueueSize = Preferences.DiskQueueSize / 1024;
DiskIoType = Preferences.DiskIoType;
DiskIoReadMode = Preferences.DiskIoReadMode;
DiskIoWriteMode = Preferences.DiskIoWriteMode;
@@ -120,8 +120,8 @@ namespace Lantean.QBTMudBlade.Components.Options
SendBufferLowWatermark = Preferences.SendBufferLowWatermark;
SendBufferWatermarkFactor = Preferences.SendBufferWatermarkFactor;
ConnectionSpeed = Preferences.ConnectionSpeed;
SocketSendBufferSize = Preferences.SocketSendBufferSize;
SocketReceiveBufferSize = Preferences.SocketReceiveBufferSize;
SocketSendBufferSize = Preferences.SocketSendBufferSize / 1024;
SocketReceiveBufferSize = Preferences.SocketReceiveBufferSize / 1024;
SocketBacklogSize = Preferences.SocketBacklogSize;
OutgoingPortsMin = Preferences.OutgoingPortsMin;
OutgoingPortsMax = Preferences.OutgoingPortsMax;
@@ -198,7 +198,7 @@ namespace Lantean.QBTMudBlade.Components.Options
protected async Task TorrentFileSizeLimitChanged(int value)
{
TorrentFileSizeLimit = value;
UpdatePreferences.TorrentFileSizeLimit = value;
UpdatePreferences.TorrentFileSizeLimit = value * 1024 * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
@@ -296,7 +296,7 @@ namespace Lantean.QBTMudBlade.Components.Options
protected async Task DiskQueueSizeChanged(int value)
{
DiskQueueSize = value;
UpdatePreferences.DiskQueueSize = value;
UpdatePreferences.DiskQueueSize = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
@@ -373,14 +373,14 @@ namespace Lantean.QBTMudBlade.Components.Options
protected async Task SocketSendBufferSizeChanged(int value)
{
SocketSendBufferSize = value;
UpdatePreferences.SocketSendBufferSize = value;
UpdatePreferences.SocketSendBufferSize = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task SocketReceiveBufferSizeChanged(int value)
{
SocketReceiveBufferSize = value;
UpdatePreferences.SocketReceiveBufferSize = value;
UpdatePreferences.SocketReceiveBufferSize = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}

View File

@@ -139,11 +139,31 @@
SlowTorrentInactiveTimer = Preferences.SlowTorrentInactiveTimer;
MaxRatioEnabled = Preferences.MaxRatioEnabled;
MaxRatio = Preferences.MaxRatio;
MaxSeedingTimeEnabled = Preferences.MaxSeedingTimeEnabled;
MaxSeedingTime = Preferences.MaxSeedingTime;
if (Preferences.MaxSeedingTimeEnabled)
{
MaxSeedingTimeEnabled = true;
MaxSeedingTime = Preferences.MaxSeedingTime;
}
else
{
MaxSeedingTimeEnabled = false;
MaxSeedingTime = 1440;
}
MaxRatioAct = Preferences.MaxRatioAct;
MaxInactiveSeedingTimeEnabled = Preferences.MaxInactiveSeedingTimeEnabled;
MaxInactiveSeedingTime = Preferences.MaxInactiveSeedingTime;
if (Preferences.MaxInactiveSeedingTimeEnabled)
{
MaxInactiveSeedingTimeEnabled = true;
MaxSeedingTime = Preferences.MaxInactiveSeedingTime;
}
else
{
MaxInactiveSeedingTimeEnabled = false;
MaxInactiveSeedingTime = 1440;
}
AddTrackersEnabled = Preferences.AddTrackersEnabled;
AddTrackers = Preferences.AddTrackers;

View File

@@ -1,4 +1,6 @@
namespace Lantean.QBTMudBlade.Components.Options
using ByteSizeLib;
namespace Lantean.QBTMudBlade.Components.Options
{
public partial class SpeedOptions : Options
{
@@ -62,10 +64,10 @@
return false;
}
UpLimit = Preferences.UpLimit;
DlLimit = Preferences.DlLimit;
AltUpLimit = Preferences.AltUpLimit;
AltDlLimit = Preferences.AltDlLimit;
UpLimit = Preferences.UpLimit / 1024;
DlLimit = Preferences.DlLimit / 1024;
AltUpLimit = Preferences.AltUpLimit / 1024;
AltDlLimit = Preferences.AltDlLimit / 1024;
BittorrentProtocol = Preferences.BittorrentProtocol;
LimitUtpRate = Preferences.LimitUtpRate;
LimitTcpOverhead = Preferences.LimitTcpOverhead;
@@ -81,28 +83,28 @@
protected async Task UpLimitChanged(int value)
{
UpLimit = value;
UpdatePreferences.UpLimit = value;
UpdatePreferences.UpLimit = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task DlLimitChanged(int value)
{
DlLimit = value;
UpdatePreferences.DlLimit = value;
UpdatePreferences.DlLimit = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AltUpLimitChanged(int value)
{
AltUpLimit = value;
UpdatePreferences.AltUpLimit = value;
UpdatePreferences.AltUpLimit = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}
protected async Task AltDlLimitChanged(int value)
{
AltDlLimit = value;
UpdatePreferences.AltDlLimit = value;
UpdatePreferences.AltDlLimit = value * 1024;
await PreferencesChanged.InvokeAsync(UpdatePreferences);
}

View File

@@ -1,4 +1,19 @@
<MudTable T="Peer" Items="Peers" >
<MudTable
Items="Peers"
T="Peer"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false">
<HeaderContent>
<MudTh>Country/Region</MudTh>
<MudTh>IP</MudTh>

View File

@@ -25,11 +25,37 @@ namespace Lantean.QBTMudBlade.Components
[EditorRequired]
public IReadOnlyList<PieceState> Pieces { get; set; } = [];
[CascadingParameter(Name = "IsDarkMode")]
public bool IsDarkMode { get; set; }
[CascadingParameter]
public MudTheme Theme { get; set; } = default!;
public Guid Id => Guid.NewGuid();
protected override async Task OnParametersSetAsync()
{
await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray());
await RenderPiecesBar();
}
private async Task RenderPiecesBar()
{
string downloadingColor;
string haveColor;
string borderColor;
if (IsDarkMode)
{
downloadingColor = Theme.PaletteDark.Success.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
haveColor = Theme.PaletteDark.Info.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
borderColor = Theme.PaletteDark.White.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
}
else
{
downloadingColor = Theme.Palette.Success.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
haveColor = Theme.Palette.Info.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
borderColor = Theme.Palette.Black.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
}
await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray(), downloadingColor, haveColor, borderColor);
}
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
@@ -50,7 +76,7 @@ namespace Lantean.QBTMudBlade.Components
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
{
await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray());
await RenderPiecesBar();
await InvokeAsync(StateHasChanged);
}

View File

@@ -0,0 +1,23 @@
@inherits MudComponentBase
<span @onclick="ToggleSortDirection" class="@Classname" style="@Style" @attributes="@UserAttributes">
@if (!AppendIcon)
{
@ChildContent
}
@if (Enabled)
{
@if (SortDirection != SortDirection.None)
{
<MudIcon Icon="@SortIcon" Class="@GetSortIconClass()" />
}
else
{
<MudIcon Icon="@SortIcon" Class="mud-table-sort-label-icon" />
}
}
@if (AppendIcon)
{
@ChildContent
}
</span>

View File

@@ -0,0 +1,96 @@
using Microsoft.AspNetCore.Components;
using MudBlazor;
using MudBlazor.Utilities;
using static MudBlazor.CategoryTypes;
namespace Lantean.QBTMudBlade.Components
{
public partial class SortLabel : MudComponentBase
{
protected string Classname => new CssBuilder("mud-button-root mud-table-sort-label")
.AddClass(Class)
.Build();
[Parameter]
public RenderFragment? ChildContent { get; set; }
[Parameter]
public SortDirection InitialDirection { get; set; } = SortDirection.None;
/// <summary>
/// Enable the sorting. Set to true by default.
/// </summary>
[Parameter]
public bool Enabled { get; set; } = true;
/// <summary>
/// Enable the sorting. Set to true by default.
/// </summary>
[Parameter]
public bool AllowUnsorted { get; set; }
/// <summary>
/// The Icon used to display SortDirection.
/// </summary>
[Parameter]
public string SortIcon { get; set; } = Icons.Material.Filled.ArrowUpward;
/// <summary>
/// If true the icon will be placed before the label text.
/// </summary>
[Parameter]
public bool AppendIcon { get; set; }
[Parameter]
public SortDirection SortDirection { get; set; }
[Parameter]
public EventCallback<SortDirection> SortDirectionChanged { get; set; }
public async Task ToggleSortDirection()
{
if (!Enabled)
{
return;
}
SortDirection sortDirection;
switch (SortDirection)
{
case SortDirection.None:
sortDirection = SortDirection.Ascending;
break;
case SortDirection.Ascending:
sortDirection = SortDirection.Descending;
break;
case SortDirection.Descending:
sortDirection = AllowUnsorted ? SortDirection.None : SortDirection.Ascending;
break;
default:
sortDirection = SortDirection.None;
break;
}
await SortDirectionChanged.InvokeAsync(sortDirection);
}
private string GetSortIconClass()
{
if (SortDirection == SortDirection.Descending)
{
return "mud-table-sort-label-icon mud-direction-desc";
}
if (SortDirection == SortDirection.Ascending)
{
return "mud-table-sort-label-icon mud-direction-asc";
}
return "mud-table-sort-label-icon";
}
}
}

View File

@@ -1,42 +1,55 @@
@if (Type == RenderType.Toolbar)
@if (RenderType == RenderType.Toolbar)
{
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true">
@ToolbarContent
</MudToolBar>
}
else if (Type == RenderType.ToolbarContents)
else if (RenderType == RenderType.ToolbarContents)
{
@ToolbarContent
}
else if (Type == RenderType.MixedToolbar)
else if (RenderType == RenderType.MixedToolbar)
{
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true">
@MixedToolbarContent
</MudToolBar>
}
else if (Type == RenderType.MixedToolbarContents)
else if (RenderType == RenderType.MixedToolbarContents)
{
@MixedToolbarContent
}
else if (Type == RenderType.InitialIconsOnly)
else if (RenderType == RenderType.InitialIconsOnly)
{
@foreach (var option in GetOptions().Take(5))
@foreach (var action in Actions.Take(5))
{
@if (option is Divider)
@if (action is Divider)
{
<MudDivider Vertical="true" />
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
<MudIconButton Title="@action.Name" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
}
}
@Menu(GetOptions().Skip(5));
@Menu(Actions.Skip(5))
}
else if (RenderType == RenderType.Children)
{
var parent = Actions.FirstOrDefault(a => a.Name == ParentAction?.Name);
if (parent is not null)
{
<MudList Clickable="true">
@foreach (var action in parent.Children)
{
<MudListItem Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
}
</MudList>
}
}
else
{
@Menu(GetOptions());
@Menu(Actions)
}
@code {
@@ -46,27 +59,27 @@ else
{
return __builder =>
{
foreach (var option in GetOptions())
foreach (var action in Actions)
{
if (option is Divider)
if (action is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
else if (!action.Children.Any())
{
if (option.Icon is null)
if (action.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback">@option.Name</MudButton>
<MudButton Color="action.Color" OnClick="action.Callback">@action.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
<MudIconButton Title="@action.Name" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
}
}
else
{
<MudMenu Icon="@option.Icon" IconColor="@option.Color" Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var childItem in option.Children)
<MudMenu Icon="@action.Icon" IconColor="@action.Color" Label="@action.Name" title="@action.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var childItem in action.Children)
{
@ChildItem(childItem)
}
@@ -83,27 +96,27 @@ else
{
return __builder =>
{
foreach (var option in GetOptions())
foreach (var action in Actions)
{
if (option is Divider)
if (action is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
else if (!action.Children.Any())
{
if (option.Icon is null)
if (action.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback" Disabled="Disabled">@option.Name</MudButton>
<MudButton Color="action.Color" OnClick="action.Callback" Disabled="Disabled">@action.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
<MudIconButton Title="@action.Name" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
}
}
else
{
<MudMenu Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" EndIcon="@Icons.Material.Filled.ArrowDropDown">
@foreach (var childItem in option.Children)
<MudMenu Label="@action.Name" title="@action.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" EndIcon="@Icons.Material.Filled.ArrowDropDown">
@foreach (var childItem in action.Children)
{
@ChildItem(childItem)
}
@@ -114,48 +127,48 @@ else
}
}
private RenderFragment ChildItem(Action option)
private RenderFragment ChildItem(TorrentAction action)
{
return __builder =>
{
if (option is Divider)
if (action is Divider)
{
<MudDivider />
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback" Disabled="Disabled">@option.Name</MudMenuItem>
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" OnTouch="action.Callback" Disabled="Disabled">@action.Name</MudMenuItem>
}
};
}
private RenderFragment Menu(IEnumerable<Action> actions)
private RenderFragment Menu(IEnumerable<TorrentAction> actions)
{
return __builder =>
{
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" Label="Actions" EndIcon="@Icons.Material.Filled.ArrowDropDown" @ref="ActionsMenu" Disabled="@(!Hashes.Any())">
@foreach (var option in actions)
@foreach (var action in actions)
{
@if (option is Divider)
@if (action is Divider)
{
<MudDivider />
}
else if (!option.Children.Any())
else if (!action.Children.Any())
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback" Disabled="Disabled">
@option.Name
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" OnTouch="action.Callback" Disabled="Disabled">
@action.Name
</MudMenuItem>
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color">
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnTouch="@(t => SubMenuTouch(action))" OnClick="@(t => SubMenuTouch(action))">
<MudMenu Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" DisableElevation="true" DisableRipple="true" Class="sub-menu">
<ActivatorContent>
@option.Name
@action.Name
</ActivatorContent>
<ChildContent>
@foreach (var childItem in option.Children)
@foreach (var childItem in action.Children)
{
@ChildItem(childItem)
}

View File

@@ -3,6 +3,7 @@ using Lantean.QBTMudBlade.Interop;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using Microsoft.JSInterop;
using MudBlazor;
@@ -39,7 +40,7 @@ namespace Lantean.QBTMudBlade.Components
/// If true this component will render as a <see cref="MudToolBar"/> otherwise will render as a <see cref="MudMenu"/>.
/// </summary>
[Parameter]
public RenderType Type { get; set; }
public RenderType RenderType { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
@@ -47,6 +48,12 @@ namespace Lantean.QBTMudBlade.Components
[CascadingParameter]
public QBitTorrentClient.Models.Preferences? Preferences { get; set; }
[Parameter]
public TorrentAction? ParentAction { get; set; }
[Parameter]
public Func<Task>? AfterAction { get; set; }
protected MudMenu? ActionsMenu { get; set; }
protected bool Disabled => !Hashes.Any();
@@ -224,6 +231,11 @@ namespace Lantean.QBTMudBlade.Components
}
}
protected async Task SubMenuTouch(TorrentAction action)
{
await DialogService.ShowSubMenu(Hashes, action);
}
private IEnumerable<Torrent> GetTorrents()
{
foreach (var hash in Hashes)
@@ -235,78 +247,104 @@ namespace Lantean.QBTMudBlade.Components
}
}
private IEnumerable<Action> GetOptions()
private List<TorrentAction>? _actions;
private IReadOnlyList<TorrentAction> Actions
{
Torrent? torrent = null;
if (Hashes.Any())
get
{
string key = Hashes.First();
if (!MainData.Torrents.TryGetValue(key, out torrent))
if (_actions is not null)
{
Hashes = Hashes.Except([key]);
return _actions;
}
}
var categories = new List<Action>
{
new Action("New", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddCategory)),
new Action("Reset", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, ResetCategory)),
new Divider()
};
categories.AddRange(MainData.Categories.Select(c => new Action(c.Value.Name, Icons.Material.Filled.List, Color.Info, EventCallback.Factory.Create(this, () => SetCategory(c.Key)))));
var tags = new List<Action>
{
new Action("Add", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddTag)),
new Action("Remove All", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, RemoveTags)),
new Divider()
};
tags.AddRange(MainData.Tags.Select(t => new Action(t, (torrent?.Tags.Contains(t) == true) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, EventCallback.Factory.Create(this, () => ToggleTag(t)))));
var options = new List<Action>
{
new Action("Pause", Icons.Material.Filled.Pause, Color.Warning, EventCallback.Factory.Create(this, Pause)),
new Action("Resume", Icons.Material.Filled.PlayArrow, Color.Success, EventCallback.Factory.Create(this, Resume)),
new Divider(),
new Action("Remove", Icons.Material.Filled.Delete, Color.Error, EventCallback.Factory.Create(this, Remove)),
new Divider(),
new Action("Set location", Icons.Material.Filled.MyLocation, Color.Info, EventCallback.Factory.Create(this, SetLocation)),
new Action("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, EventCallback.Factory.Create(this, Rename)),
new Action("Category", Icons.Material.Filled.List, Color.Info, categories, true),
new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags, true),
new Action("Automatic Torrent Management", Icons.Material.Filled.Check, (torrent?.AutomaticTorrentManagement == true) ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)),
new Divider(),
new Action("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, EventCallback.Factory.Create(this, LimitUploadRate)),
new Action("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, EventCallback.Factory.Create(this, LimitShareRatio)),
new Action("Super seeding mode", Icons.Material.Filled.Check, (torrent?.SuperSeeding == true) ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleSuperSeeding)),
new Divider(),
new Action("Force recheck", Icons.Material.Filled.Loop, Color.Info, EventCallback.Factory.Create(this, ForceRecheck)),
new Action("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, EventCallback.Factory.Create(this, ForceReannounce)),
new Divider(),
new Action("Queue", Icons.Material.Filled.Queue, Color.Transparent, new List<Action>
Torrent? torrent = null;
if (Hashes.Any())
{
new Action("Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, EventCallback.Factory.Create(this, MoveToTop)),
new Action("Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, EventCallback.Factory.Create(this, MoveUp)),
new Action("Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, EventCallback.Factory.Create(this, MoveDown)),
new Action("Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, EventCallback.Factory.Create(this, MoveToBottom)),
}),
new Action("Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List<Action>
string key = Hashes.First();
if (!MainData.Torrents.TryGetValue(key, out torrent))
{
Hashes = Hashes.Except([key]);
}
}
var categories = new List<TorrentAction>
{
new Action("Name", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(t => t.Name))),
new Action("Info hash v1", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(t => t.InfoHashV1))),
new Action("Info hash v2", Icons.Material.Filled.Tag, Color.Info, EventCallback.Factory.Create(this, () => Copy(t => t.InfoHashV2))),
new Action("Magnet link", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(t => t.MagnetUri))),
new Action("Torrent ID", Icons.Material.Filled.TextFields, Color.Info, EventCallback.Factory.Create(this, () => Copy(t => t.Hash))),
}),
new Action("Export", Icons.Material.Filled.SaveAlt, Color.Info, EventCallback.Factory.Create(this, Export)),
};
new TorrentAction("New", Icons.Material.Filled.Add, Color.Info, CreateCallback(AddCategory)),
new TorrentAction("Reset", Icons.Material.Filled.Remove, Color.Error, CreateCallback(ResetCategory)),
new Divider()
};
categories.AddRange(MainData.Categories.Select(c => new TorrentAction(c.Value.Name, Icons.Material.Filled.List, Color.Info, CreateCallback(() => SetCategory(c.Key)))));
if (Preferences?.QueueingEnabled == false)
{
options.RemoveAt(18);
var tags = new List<TorrentAction>
{
new TorrentAction("Add", Icons.Material.Filled.Add, Color.Info, CreateCallback(AddTag)),
new TorrentAction("Remove All", Icons.Material.Filled.Remove, Color.Error, CreateCallback(RemoveTags)),
new Divider()
};
tags.AddRange(MainData.Tags.Select(t => new TorrentAction(t, (torrent?.Tags.Contains(t) == true) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, CreateCallback(() => ToggleTag(t)))));
_actions = new List<TorrentAction>
{
new TorrentAction("Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
new TorrentAction("Resume", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new Divider(),
new TorrentAction("Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove)),
new Divider(),
new TorrentAction("Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation)),
new TorrentAction("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)),
new TorrentAction("Category", Icons.Material.Filled.List, Color.Info, categories, true),
new TorrentAction("Tags", Icons.Material.Filled.Label, Color.Info, tags, true),
new TorrentAction("Automatic Torrent Management", Icons.Material.Filled.Check, (torrent?.AutomaticTorrentManagement == true) ? Color.Info : Color.Transparent, CreateCallback(ToggleAutoTMM)),
new Divider(),
new TorrentAction("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, CreateCallback(LimitUploadRate)),
new TorrentAction("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, CreateCallback(LimitShareRatio)),
new TorrentAction("Super seeding mode", Icons.Material.Filled.Check, (torrent?.SuperSeeding == true) ? Color.Info : Color.Transparent, CreateCallback(ToggleSuperSeeding)),
new Divider(),
new TorrentAction("Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck)),
new TorrentAction("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)),
new Divider(),
new TorrentAction("Queue", Icons.Material.Filled.Queue, Color.Transparent, new List<TorrentAction>
{
new TorrentAction("Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)),
new TorrentAction("Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)),
new TorrentAction("Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)),
new TorrentAction("Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)),
}),
new TorrentAction("Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List<TorrentAction>
{
new TorrentAction("Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))),
new TorrentAction("Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))),
new TorrentAction("Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
new TorrentAction("Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
new TorrentAction("Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
}),
new TorrentAction("Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
};
if (Preferences?.QueueingEnabled == false)
{
_actions.RemoveAt(18);
}
return _actions;
}
}
return options;
private EventCallback CreateCallback(Func<Task> action)
{
if (AfterAction is not null)
{
return EventCallback.Factory.Create(this, async () =>
{
await action();
await AfterAction();
});
}
else
{
return EventCallback.Factory.Create(this, action);
}
}
}
@@ -333,18 +371,19 @@ namespace Lantean.QBTMudBlade.Components
/// </summary>
MixedToolbar,
InitialIconsOnly,
Children,
}
public class Divider : Action
public class Divider : TorrentAction
{
public Divider() : base("-", default!, Color.Default, default(EventCallback))
{
}
}
public class Action
public class TorrentAction
{
public Action(string name, string? icon, Color color, EventCallback callback)
public TorrentAction(string name, string? icon, Color color, EventCallback callback)
{
Name = name;
Icon = icon;
@@ -353,7 +392,7 @@ namespace Lantean.QBTMudBlade.Components
Children = [];
}
public Action(string name, string? icon, Color color, IEnumerable<Action> children, bool useTextButton = false)
public TorrentAction(string name, string? icon, Color color, IEnumerable<TorrentAction> children, bool useTextButton = false)
{
Name = name;
Icon = icon;
@@ -371,7 +410,7 @@ namespace Lantean.QBTMudBlade.Components
public EventCallback Callback { get; }
public IEnumerable<Action> Children { get; }
public IEnumerable<TorrentAction> Children { get; }
public bool UseTextButton { get; }
}

View File

@@ -1,4 +1,19 @@
<MudTable T="Lantean.QBitTorrentClient.Models.TorrentTrackers" Items="Trackers" >
<MudTable
T="Lantean.QBitTorrentClient.Models.TorrentTrackers"
Items="Trackers"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false">
<HeaderContent>
<MudTh>Tier</MudTh>
<MudTh>URL</MudTh>

View File

@@ -1,4 +1,19 @@
<MudTable T="Lantean.QBitTorrentClient.Models.WebSeed" Items="WebSeeds" >
<MudTable
T="Lantean.QBitTorrentClient.Models.WebSeed"
Items="WebSeeds"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Striped="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false">
<HeaderContent>
<MudTh>URL</MudTh>
</HeaderContent>

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Filter;
using Lantean.QBTMudBlade.Models;
@@ -12,6 +13,8 @@ namespace Lantean.QBTMudBlade
private static readonly DialogOptions _confirmDialogOptions = new() { ClassBackground = "background-blur" };
public const long _maxFileSize = 4194304;
public static async Task InvokeAddTorrentFileDialog(this IDialogService dialogService, IApiClient apiClient)
{
var result = await dialogService.ShowAsync<AddTorrentFileDialog>("Upload local torrent", FormDialogOptions);
@@ -28,7 +31,7 @@ namespace Lantean.QBTMudBlade
var files = new Dictionary<string, Stream>();
foreach (var file in options.Files)
{
var stream = file.OpenReadStream();
var stream = file.OpenReadStream(_maxFileSize);
streams.Add(stream);
files.Add(file.Name, stream);
}
@@ -142,7 +145,7 @@ namespace Lantean.QBTMudBlade
await onSuccess();
}
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Action onSuccess)
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, System.Action onSuccess)
{
await ShowConfirmDialog(dialogService, title, content, () =>
{
@@ -175,8 +178,8 @@ namespace Lantean.QBTMudBlade
var parameters = new DialogParameters
{
{ nameof(SliderFieldDialog<long>.Value), rate },
{ nameof(SliderFieldDialog<long>.Min), 0 },
{ nameof(SliderFieldDialog<long>.Max), 100 },
{ nameof(SliderFieldDialog<long>.Min), 0L },
{ nameof(SliderFieldDialog<long>.Max), 100L },
};
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions);
@@ -194,8 +197,8 @@ namespace Lantean.QBTMudBlade
var parameters = new DialogParameters
{
{ nameof(SliderFieldDialog<float>.Value), ratio },
{ nameof(SliderFieldDialog<float>.Min), 0 },
{ nameof(SliderFieldDialog<float>.Max), 100 },
{ nameof(SliderFieldDialog<float>.Min), 0F },
{ nameof(SliderFieldDialog<float>.Max), 100F },
};
var result = await dialogService.ShowAsync<SliderFieldDialog<float>>("Upload Rate", parameters, FormDialogOptions);
@@ -247,5 +250,16 @@ namespace Lantean.QBTMudBlade
{
await Task.Delay(0);
}
public static async Task ShowSubMenu(this IDialogService dialogService, IEnumerable<string> hashes, TorrentAction parent)
{
var parameters = new DialogParameters
{
{ nameof(SubMenuDialog.ParentAction), parent },
{ nameof(SubMenuDialog.Hashes), hashes }
};
await dialogService.ShowAsync<SubMenuDialog>("Actions", parameters, FormDialogOptions);
}
}
}

View File

@@ -33,7 +33,15 @@ namespace Lantean.QBTMudBlade
return "< 1m";
}
var time = TimeSpan.FromSeconds(seconds.Value);
TimeSpan time;
try
{
time = TimeSpan.FromSeconds(seconds.Value);
}
catch (OverflowException)
{
return "∞";
}
var sb = new StringBuilder();
if (prefix is not null)
{

View File

@@ -41,8 +41,10 @@ namespace Lantean.QBTMudBlade.Filter
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && ((string)x).StartsWith(value, stringComparer))),
FilterOperator.String.EndsWith =>
propertyExpression.Modify<T>((Expression<Func<object?, bool>>)(x => (string?)x != null && value != null && ((string)x).EndsWith(value, stringComparer))),
FilterOperator.String.Empty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => string.IsNullOrWhiteSpace(x))),
FilterOperator.String.NotEmpty => propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => !string.IsNullOrWhiteSpace(x))),
FilterOperator.String.Empty =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => string.IsNullOrWhiteSpace(x))),
FilterOperator.String.NotEmpty =>
propertyExpression.Modify<T>((Expression<Func<string?, bool>>)(x => !string.IsNullOrWhiteSpace(x))),
_ => x => true
};
}

View File

@@ -67,8 +67,8 @@ namespace Lantean.QBTMudBlade.Filter
{
if (fieldType.IsString)
{
return new[]
{
return
[
String.Contains,
String.NotContains,
String.Equal,
@@ -77,12 +77,12 @@ namespace Lantean.QBTMudBlade.Filter
String.EndsWith,
String.Empty,
String.NotEmpty,
};
];
}
if (fieldType.IsNumber)
{
return new[]
{
return
[
Number.Equal,
Number.NotEqual,
Number.GreaterThan,
@@ -91,26 +91,27 @@ namespace Lantean.QBTMudBlade.Filter
Number.LessThanOrEqual,
Number.Empty,
Number.NotEmpty,
};
];
}
if (fieldType.IsEnum)
{
return new[] {
return
[
Enum.Is,
Enum.IsNot,
};
];
}
if (fieldType.IsBoolean)
{
return new[]
{
return
[
Boolean.Is,
};
];
}
if (fieldType.IsDateTime)
{
return new[]
{
return
[
DateTime.Is,
DateTime.IsNot,
DateTime.After,
@@ -119,19 +120,19 @@ namespace Lantean.QBTMudBlade.Filter
DateTime.OnOrBefore,
DateTime.Empty,
DateTime.NotEmpty,
};
];
}
if (fieldType.IsGuid)
{
return new[]
{
return
[
Guid.Equal,
Guid.NotEqual,
};
];
}
// default
return Array.Empty<string>();
return [];
}
}
}

View File

@@ -1,6 +1,4 @@
using MudBlazor;
using System.Linq.Expressions;
using System.Reflection;
using System.Linq.Expressions;
namespace Lantean.QBTMudBlade.Filter
{
@@ -27,6 +25,4 @@ namespace Lantean.QBTMudBlade.Filter
public Expression<Func<T, object?>> Expression { get; }
}
}
}

View File

@@ -166,7 +166,12 @@ namespace Lantean.QBTMudBlade
public static bool FilterStatus(Torrent torrent, Status status)
{
var state = torrent.State;
return FilterStatus(torrent.State, torrent.UploadSpeed, status);
}
public static bool FilterStatus(string state, long uploadSpeed, Status status)
{
bool inactive = false;
switch (status)
{
@@ -188,10 +193,11 @@ namespace Lantean.QBTMudBlade
break;
case Status.Completed:
if (state != "uploading" && !state.Contains("UL"))
if ((state != "uploading") && (!state.Contains("UP")))
{
return false;
}
break;
case Status.Resumed:
@@ -217,7 +223,7 @@ namespace Lantean.QBTMudBlade
bool check;
if (state == "stalledDL")
{
check = torrent.UploadSpeed > 0;
check = uploadSpeed > 0;
}
else
{

View File

@@ -24,9 +24,9 @@ namespace Lantean.QBTMudBlade.Interop
await runtime.InvokeVoidAsync("qbt.open", url, target);
}
public static async Task RenderPiecesBar(this IJSRuntime runtime, string id, string hash, int[] pieces)
public static async Task RenderPiecesBar(this IJSRuntime runtime, string id, string hash, int[] pieces, string? downloadingColor = null, string? haveColor = null, string? borderColor = null)
{
await runtime.InvokeVoidAsync("qbt.renderPiecesBar", id, hash, pieces);
await runtime.InvokeVoidAsync("qbt.renderPiecesBar", id, hash, pieces, downloadingColor, haveColor, borderColor );
}
}
}

View File

@@ -5,6 +5,7 @@
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CompressionEnabled>false</CompressionEnabled>
</PropertyGroup>
<ItemGroup>
@@ -14,6 +15,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.4" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="MudBlazor" Version="6.19.1" />
<PackageReference Include="MudBlazor.ThemeManager" Version="1.0.9" />
</ItemGroup>
<ItemGroup>

View File

@@ -2,14 +2,14 @@
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" />
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar Elevation="1">
<MudAppBar>
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@@ -19,6 +19,7 @@
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge>
}
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" @bind-Value="IsDarkMode" />
@if (ShowMenu)
{
<Menu />
@@ -27,9 +28,11 @@
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
<CascadingValue Value="Theme">
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</MudLayout>

View File

@@ -1,7 +1,6 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using MudBlazor.Services;
@@ -40,6 +39,14 @@ namespace Lantean.QBTMudBlade.Layout
NotifyOnBreakpointOnly = true
};
protected MudTheme Theme { get; set; }
public MainLayout()
{
Theme = new MudTheme();
Theme.Typography.Default.FontFamily = ["Nunito Sans"];
}
protected void ToggleDrawer()
{
DrawerOpen = !DrawerOpen;
@@ -64,13 +71,13 @@ namespace Lantean.QBTMudBlade.Layout
}
}
protected async Task OnSystemPreferenceChanged(bool value)
protected Task OnSystemPreferenceChanged(bool value)
{
IsDarkMode = value;
await InvokeAsync(StateHasChanged);
return Task.CompletedTask;
}
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
{
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
{
@@ -80,7 +87,8 @@ namespace Lantean.QBTMudBlade.Layout
{
DrawerOpen = true;
}
await InvokeAsync(StateHasChanged);
return Task.CompletedTask;
}
protected void ToggleErrorDrawer()
@@ -113,4 +121,4 @@ namespace Lantean.QBTMudBlade.Layout
GC.SuppressFinalize(this);
}
}
}
}

View File

@@ -45,7 +45,7 @@ namespace Lantean.QBTMudBlade.Models
public long Downloaded => (long)Math.Round(Size * Progress, 0);
public long Remaining => Size - Downloaded;
public long Remaining => Progress == 1 ? 0 : Size - Downloaded;
public bool IsFolder { get; }

View File

@@ -9,28 +9,31 @@
}
@if (Hash is not null)
{
<TorrentActions Type="RenderType.InitialIconsOnly" Hashes="@([Hash])" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="@([Hash])" />
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>
</MudToolBar>
<CascadingValue Value="RefreshInterval">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
<MudTabPanel Text="General">
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
</MudTabPanel>
<MudTabPanel Text="Trackers">
<TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
</MudTabPanel>
<MudTabPanel Text="Peers">
<PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
</MudTabPanel>
<MudTabPanel Text="HTTP Sources">
<WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
</MudTabPanel>
<MudTabPanel Text="Content">
<FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
</MudTabPanel>
</MudTabs>
</CascadingValue>
@if (ShowTabs)
{
<CascadingValue Value="RefreshInterval">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
<MudTabPanel Text="General">
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />
</MudTabPanel>
<MudTabPanel Text="Trackers">
<TrackersTab Hash="@Hash" Active="@(ActiveTab == 1)" />
</MudTabPanel>
<MudTabPanel Text="Peers">
<PeersTab Hash="@Hash" Active="@(ActiveTab == 2)" />
</MudTabPanel>
<MudTabPanel Text="HTTP Sources">
<WebSeedsTab Hash="@Hash" Active="@(ActiveTab == 3)" />
</MudTabPanel>
<MudTabPanel Text="Content">
<FilesTab Hash="@Hash" Active="@(ActiveTab == 4)" />
</MudTabPanel>
</MudTabs>
</CascadingValue>
}

View File

@@ -1,5 +1,4 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using MudBlazor;
@@ -32,6 +31,8 @@ namespace Lantean.QBTMudBlade.Pages
protected string Name => GetName();
protected bool ShowTabs { get; set; } = true;
private string GetName()
{
if (Hash is null || MainData is null)
@@ -47,45 +48,6 @@ namespace Lantean.QBTMudBlade.Pages
return torrent.Name;
}
protected async Task PauseTorrent()
{
if (Hash is null)
{
return;
}
await ApiClient.PauseTorrent(Hash);
}
protected async Task ResumeTorrent()
{
if (Hash is null)
{
return;
}
await ApiClient.ResumeTorrent(Hash);
}
protected async Task RemoveTorrent()
{
if (Hash is null)
{
return;
}
var reference = await DialogService.ShowAsync<DeleteDialog>("Remove torrent(s)?");
var result = await reference.Result;
if (result.Canceled)
{
return;
}
await ApiClient.DeleteTorrent(Hash, (bool)result.Data);
NavigationManager.NavigateTo("/");
}
protected void NavigateBack()
{
NavigationManager.NavigateTo("/");

View File

@@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG
protected override async Task OnInitializedAsync()
{
await DoLogin("admin", "rp46PuPVn");
await DoLogin("admin", "u4FR4ZQCm");
}
#endif
}

View File

@@ -1,4 +0,0 @@
@page "/main"
<PageTitle>qBittorrent @Version Web UI</PageTitle>

View File

@@ -1,154 +0,0 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components;
using System.Net;
namespace Lantean.QBTMudBlade.Pages
{
public partial class Main : IDisposable
{
private bool _refreshEnabled = true;
protected bool DrawerOpen { get; set; } = true;
protected int RefreshInterval { get; set; } = 1500;
private int _requestId = 0;
private bool _disposedValue;
private readonly CancellationTokenSource _timerCancellationToken = new();
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDataManager DataManager { get; set; } = default!;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
protected Models.MainData? TorrentList { get; set; }
protected string Category { get; set; } = FilterHelper.CATEGORY_ALL;
protected string Tag { get; set; } = FilterHelper.TAG_ALL;
protected string Tracker { get; set; } = FilterHelper.TRACKER_ALL;
protected Status Status { get; set; } = Status.All;
protected string? SearchText { get; set; }
protected FilterState FilterState => new FilterState(Category, Status, Tag, Tracker, TorrentList?.ServerState.UseSubcategories ?? false, SearchText);
protected string? Version { get; set; }
private async Task SearchTextChanged(string searchText)
{
SearchText = searchText == "" ? null : searchText;
await InvokeAsync(StateHasChanged);
}
protected async Task SelectedTorrentChanged(string hash)
{
if (TorrentList is not null)
{
TorrentList.SelectedTorrentHash = hash;
}
await InvokeAsync(StateHasChanged);
}
protected override async Task OnInitializedAsync()
{
if (!await ApiClient.CheckAuthState())
{
NavigationManager.NavigateTo("/login");
return;
}
try
{
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
TorrentList = DataManager.CreateMainData(data);
_requestId = data.ResponseId;
RefreshInterval = TorrentList.ServerState.RefreshInterval;
await InvokeAsync(StateHasChanged);
}
catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Forbidden)
{
NavigationManager.NavigateTo("/login");
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_refreshEnabled)
{
return;
}
if (firstRender)
{
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
QBitTorrentClient.Models.MainData data;
try
{
data = await ApiClient.GetMainData(_requestId);
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
if (TorrentList is null || data.FullUpdate)
{
TorrentList = DataManager.CreateMainData(data);
}
else
{
DataManager.MergeMainData(data, TorrentList);
}
RefreshInterval = TorrentList.ServerState.RefreshInterval;
_requestId = data.ResponseId;
await InvokeAsync(StateHasChanged);
}
}
}
}
protected void DrawerToggle()
{
DrawerOpen = !DrawerOpen;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View File

@@ -5,7 +5,7 @@
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" Title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" Title="Add torrent file" />
<MudDivider Vertical="true" />
<TorrentActions Type="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrents()" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrents()" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrent" Title="View torrent details" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" />

View File

@@ -4,7 +4,6 @@ using Lantean.QBTMudBlade.Components;
using Lantean.QBTMudBlade.Components.Dialogs;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Pages
@@ -171,8 +170,8 @@ namespace Lantean.QBTMudBlade.Pages
public static List<ColumnDefinition<Torrent>> ColumnsDefinitions { get; } =
[
CreateColumnDefinition("#", t => t.Priority),
CreateColumnDefinition("", t => t.State, IconColumn),
CreateColumnDefinition("Name", t => t.Name, width: 200),
CreateColumnDefinition("", t => t.State, IconColumn, iconOnly: true),
CreateColumnDefinition("Name", t => t.Name, width: 400),
CreateColumnDefinition("Size", t => t.Size, t => DisplayHelpers.Size(t.Size)),
CreateColumnDefinition("Total Size", t => t.TotalSize, t => DisplayHelpers.Size(t.TotalSize), enabled: false),
CreateColumnDefinition("Done", t => t.Progress, ProgressBarColumn, tdClass: "table-progress pl-1 pr-1"),
@@ -205,7 +204,7 @@ namespace Lantean.QBTMudBlade.Pages
//CreateColumnDefinition("Reannounce In", t => t.Reannounce, enabled: false),
];
private static ColumnDefinition<Torrent> CreateColumnDefinition(string name, Func<Torrent, object?> selector, RenderFragment<RowContext<Torrent>> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true)
private static ColumnDefinition<Torrent> CreateColumnDefinition(string name, Func<Torrent, object?> selector, RenderFragment<RowContext<Torrent>> rowTemplate, int? width = null, string? tdClass = null, bool enabled = true, bool iconOnly = false)
{
var cd = new ColumnDefinition<Torrent>(name, selector, rowTemplate);
cd.Class = "no-wrap";
@@ -215,11 +214,12 @@ namespace Lantean.QBTMudBlade.Pages
}
cd.Width = width;
cd.Enabled = enabled;
cd.IconOnly = iconOnly;
return cd;
}
private static ColumnDefinition<Torrent> CreateColumnDefinition(string name, Func<Torrent, object?> selector, Func<Torrent, string>? formatter = null, int? width = null, string? tdClass = null, bool enabled = true)
private static ColumnDefinition<Torrent> CreateColumnDefinition(string name, Func<Torrent, object?> selector, Func<Torrent, string>? formatter = null, int? width = null, string? tdClass = null, bool enabled = true, bool iconOnly = false)
{
var cd = new ColumnDefinition<Torrent>(name, selector, formatter);
cd.Class = "no-wrap";
@@ -229,6 +229,7 @@ namespace Lantean.QBTMudBlade.Pages
}
cd.Width = width;
cd.Enabled = enabled;
cd.IconOnly = iconOnly;
return cd;
}

View File

@@ -1,8 +1,4 @@
using Lantean.QBTMudBlade.Models;
using MudBlazor;
using System.Linq;
using System.Reflection;
using System.Threading.Channels;
namespace Lantean.QBTMudBlade.Services
{
@@ -563,8 +559,12 @@ namespace Lantean.QBTMudBlade.Services
public Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files)
{
var contents = new Dictionary<string, ContentItem>();
if (files.Count == 0)
{
return contents;
}
var folderIndex = files.Max(f => f.Index) + 1;
var folderIndex = files.Min(f => f.Index) - 1;
foreach (var file in files)
{
@@ -582,7 +582,7 @@ namespace Lantean.QBTMudBlade.Services
var directoryPath = string.Join(Extensions.DirectorySeparator, paths[0..(i + 1)]);
if (!contents.ContainsKey(directoryPath))
{
contents.Add(directoryPath, new ContentItem(directoryPath, directoryName, folderIndex++, Priority.Normal, 0, 0, 0, true, i));
contents.Add(directoryPath, new ContentItem(directoryPath, directoryName, folderIndex--, Priority.Normal, 0, 0, 0, true, i));
}
}
@@ -600,13 +600,22 @@ namespace Lantean.QBTMudBlade.Services
var level = directory.Value.Level;
var filesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && !c.Value.IsFolder).ToList();
var directoriesContents = contents.Where(c => c.Value.Name.StartsWith(key + Extensions.DirectorySeparator) && c.Value.IsFolder && c.Value.Level == level + 1).ToList();
var directoryContents = filesContents.Concat(directoriesContents);
var allContents = filesContents.Concat(directoriesContents);
var priorities = allContents.Select(d => d.Value.Priority).Distinct();
var downloadingContents = allContents.Where(c => c.Value.Priority != Priority.DoNotDownload).ToList();
var size = directoryContents.Sum(c => c.Value.Size);
var availability = directoryContents.Average(c => c.Value.Availability);
var downloaded = directoryContents.Sum(c => c.Value.Downloaded);
var progress = (float)downloaded / size;
long size = 0;
float availability = 0;
long downloaded = 0;
float progress = 0;
if (downloadingContents.Count != 0)
{
size = downloadingContents.Sum(c => c.Value.Size);
availability = downloadingContents.Average(c => c.Value.Availability);
downloaded = downloadingContents.Sum(c => c.Value.Downloaded);
progress = (float)downloaded / size;
}
if (!contents.TryGetValue(key, out var dir))
{
@@ -615,7 +624,6 @@ namespace Lantean.QBTMudBlade.Services
dir.Availability = availability;
dir.Size = size;
dir.Progress = progress;
var priorities = directoryContents.Select(d => d.Value.Priority).Distinct();
if (priorities.Count() == 1)
{
dir.Priority = priorities.First();

View File

@@ -42,6 +42,8 @@ namespace Lantean.QBTMudBlade
public RenderFragment<RowContext<T>> RowTemplate { get; set; }
public bool IconOnly { get; set; }
public int? Width { get; set; }
public Func<T, string>? Formatter { get; set; }

View File

@@ -99,20 +99,12 @@ td.no-wrap {
text-overflow: ellipsis; /* Display an ellipsis when the text overflows */
}
.rotate-180 {
transform: rotate(180deg);
}
.rotate-90 {
transform: rotate(90deg);
}
.background-blur {
backdrop-filter: blur(10px);
}
.icon-menu::after {
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E");
content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h24v24H0V0z' fill='#27272f'/%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E");
width: 25px;
height: 25px;
position: absolute;

View File

@@ -6,7 +6,9 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>qBittorrent Web UI</title>
<base href="/" />
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
<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 rel="icon" type="image/png" href="favicon.png" />

View File

@@ -20,7 +20,7 @@ window.qbt.open = (url, target) => {
window.open(url, target);
}
window.qbt.renderPiecesBar = (id, hash, pieces) => {
window.qbt.renderPiecesBar = (id, hash, pieces, downloadingColor, haveColor, borderColor) => {
const parentElement = document.getElementById(id);
if (window.qbt.hash !== hash) {
if (parentElement) {
@@ -29,9 +29,19 @@ window.qbt.renderPiecesBar = (id, hash, pieces) => {
}
}
window.qbt.hash = hash;
window.qbt.piecesBar = new window.qbt.PiecesBar([], {
const options = {
height: 24
});
};
if (downloadingColor) {
options.downloadingColor = downloadingColor;
}
if (haveColor) {
options.haveColor = haveColor;
}
if (borderColor) {
options.borderColor = borderColor;
}
window.qbt.piecesBar = new window.qbt.PiecesBar([], options);
window.qbt.piecesBar.clear();
}