mirror of
https://github.com/lantean-code/qbtmud.git
synced 2025-10-23 04:52:22 +00:00
Compare commits
14 Commits
1.1.0
...
300e81345c
Author | SHA1 | Date | |
---|---|---|---|
|
300e81345c | ||
|
4824037ba7 | ||
|
1f9b631a36 | ||
|
2c744cd972 | ||
|
b02bb7cfae | ||
|
e4dac8556e | ||
|
a9a8a4eba8 | ||
|
bb524450f0 | ||
|
d4ac79af00 | ||
|
7370d73c59 | ||
|
8796cc0f24 | ||
|
b24ae440d4 | ||
|
bb90ce5216 | ||
|
1cf9f97187 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -360,4 +360,5 @@ MigrationBackup/
|
||||
.ionide/
|
||||
|
||||
# Fody - auto-generated XML schema
|
||||
FodyWeavers.xsd
|
||||
FodyWeavers.xsd
|
||||
/output
|
||||
|
@@ -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")),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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;
|
||||
|
@@ -62,7 +62,7 @@
|
||||
<MudCardContent Class="pt-0">
|
||||
<MudGrid>
|
||||
<MudItem xs="12">
|
||||
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoDeleteModeChanged" Variant="Variant.Outlined">
|
||||
<MudSelect T="bool" Label="Default Torrent Management Mode" Value="AutoTmmEnabled" ValueChanged="AutoTmmEnabledChanged" Variant="Variant.Outlined">
|
||||
<MudSelectItem Value="false">Manual</MudSelectItem>
|
||||
<MudSelectItem Value="true">Automatic</MudSelectItem>
|
||||
</MudSelect>
|
||||
|
@@ -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
|
||||
{
|
||||
@@ -81,6 +82,8 @@ 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 = [];
|
||||
@@ -91,6 +94,12 @@ namespace Lantean.QBTMud.Components.UI
|
||||
|
||||
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;
|
||||
@@ -109,6 +118,13 @@ namespace Lantean.QBTMud.Components.UI
|
||||
SelectedColumns = selectedColumns;
|
||||
await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedColumns = selectedColumns;
|
||||
}
|
||||
|
||||
_lastColumnDefinitions = ColumnDefinitions;
|
||||
MarkColumnsDirty();
|
||||
|
||||
string? sortColumn;
|
||||
SortDirection sortDirection;
|
||||
@@ -137,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()
|
||||
@@ -165,39 +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);
|
||||
if (_columnOrder.Count == 0)
|
||||
if (!_columnsDirty)
|
||||
{
|
||||
foreach (var column in filteredColumns)
|
||||
{
|
||||
if (_columnWidths.TryGetValue(column.Id, out var value))
|
||||
{
|
||||
column.Width = value;
|
||||
}
|
||||
|
||||
yield return column;
|
||||
}
|
||||
|
||||
yield break;
|
||||
return _visibleColumns;
|
||||
}
|
||||
|
||||
var columnDictionary = filteredColumns.ToDictionary(c => c.Id);
|
||||
foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key))
|
||||
_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)
|
||||
{
|
||||
if (!columnDictionary.TryGetValue(columnId, out var column))
|
||||
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)
|
||||
{
|
||||
continue;
|
||||
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)
|
||||
@@ -316,18 +380,21 @@ 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -379,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)
|
||||
@@ -392,4 +465,4 @@ namespace Lantean.QBTMud.Components.UI
|
||||
public SortDirection SortDirection { get; init; }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -19,28 +19,28 @@ namespace Lantean.QBTMud.Helpers
|
||||
{
|
||||
if (seconds is null)
|
||||
{
|
||||
return "";
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
if (seconds == 8640000)
|
||||
const long InfiniteEtaSentinelSeconds = 8_640_000; // ~100 days, used by qBittorrent for "infinite" ETA.
|
||||
var value = seconds.Value;
|
||||
|
||||
if (value >= long.MaxValue || value >= TimeSpan.MaxValue.TotalSeconds || value == InfiniteEtaSentinelSeconds)
|
||||
{
|
||||
return "∞";
|
||||
}
|
||||
|
||||
if (seconds < 60)
|
||||
if (value <= 0)
|
||||
{
|
||||
return "< 1m";
|
||||
}
|
||||
|
||||
TimeSpan time;
|
||||
try
|
||||
var time = TimeSpan.FromSeconds(value);
|
||||
if (time.TotalMinutes < 1)
|
||||
{
|
||||
time = TimeSpan.FromSeconds(seconds.Value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return "∞";
|
||||
return "< 1m";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
if (prefix is not null)
|
||||
{
|
||||
@@ -83,6 +83,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Formats a file size in bytes into an appropriate unit based on the size.
|
||||
/// </summary>
|
||||
|
@@ -119,34 +119,34 @@ namespace Lantean.QBTMud.Helpers
|
||||
switch (category)
|
||||
{
|
||||
case CATEGORY_ALL:
|
||||
break;
|
||||
return true;
|
||||
|
||||
case CATEGORY_UNCATEGORIZED:
|
||||
if (!string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
|
||||
return true;
|
||||
default:
|
||||
if (string.IsNullOrEmpty(torrent.Category))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!useSubcategories)
|
||||
{
|
||||
if (torrent.Category != category)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!torrent.Category.StartsWith(category))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return string.Equals(torrent.Category, category, StringComparison.Ordinal);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return true;
|
||||
if (string.Equals(torrent.Category, category, StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var prefix = string.Concat(category, "/");
|
||||
return torrent.Category.StartsWith(prefix, StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool FilterTag(Torrent torrent, string tag)
|
||||
@@ -207,7 +207,7 @@ namespace Lantean.QBTMud.Helpers
|
||||
break;
|
||||
|
||||
case Status.Paused:
|
||||
if (!state.Contains("paused") || !state.Contains("stopped"))
|
||||
if (!state.Contains("paused") && !state.Contains("stopped"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
@@ -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">
|
||||
@@ -49,7 +50,7 @@
|
||||
@{
|
||||
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
|
||||
}
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="@MainData?.ServerState.ConnectionStatus" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
|
||||
<MudDivider Vertical="true" Class="" />
|
||||
@@ -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()
|
||||
@@ -84,6 +100,7 @@ namespace Lantean.QBTMud.Layout
|
||||
Version = await ApiClient.GetApplicationVersion();
|
||||
var data = await ApiClient.GetMainData(_requestId);
|
||||
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, 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);
|
||||
|
||||
@@ -159,14 +195,84 @@ namespace Lantean.QBTMud.Layout
|
||||
|
||||
protected static (string, Color) GetConnectionIcon(string? status)
|
||||
{
|
||||
if (status is null)
|
||||
return status switch
|
||||
{
|
||||
return (Icons.Material.Outlined.SignalWifiOff, Color.Warning);
|
||||
"firewalled" => (Icons.Material.Outlined.SignalWifiStatusbarConnectedNoInternet4, Color.Warning),
|
||||
"connected" => (Icons.Material.Outlined.SignalWifi4Bar, Color.Success),
|
||||
_ => (Icons.Material.Outlined.SignalWifiOff, Color.Error),
|
||||
};
|
||||
}
|
||||
|
||||
private void OnCategoryChanged(string category)
|
||||
{
|
||||
if (Category == category)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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)
|
||||
@@ -248,4 +335,5 @@ namespace Lantean.QBTMud.Pages
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -8,7 +8,7 @@ namespace Lantean.QBTMud.Services
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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"
|
||||
}
|
||||
}
|
@@ -68,11 +68,13 @@ cd qbtmud
|
||||
dotnet restore
|
||||
```
|
||||
|
||||
### 3. Build the Application
|
||||
### 3. Build and Publish the Application
|
||||
```sh
|
||||
dotnet build --configuration Release
|
||||
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.
|
||||
|
||||
|
Reference in New Issue
Block a user