4 Commits

Author SHA1 Message Date
ahjephson
2c744cd972 Fix issue wtih toolbar 2025-10-19 19:13:09 +01:00
ahjephson
b02bb7cfae Fix issues with toolbar not updating 2025-10-19 19:12:10 +01:00
ahjephson
e4dac8556e Improve torrent list performance 2025-10-19 15:21:22 +01:00
ahjephson
a9a8a4eba8 Improve file list performance. 2025-10-19 14:19:21 +01:00
9 changed files with 1217 additions and 206 deletions

3
.gitignore vendored
View File

@@ -360,4 +360,5 @@ MigrationBackup/
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
FodyWeavers.xsd
/output

View File

@@ -21,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 = [];
@@ -103,6 +106,7 @@ namespace Lantean.QBTMud.Components
if (_filterDefinitions is null)
{
Filters = null;
MarkFilesDirty();
return;
}
@@ -114,11 +118,13 @@ namespace Lantean.QBTMud.Components
}
Filters = filters;
MarkFilesDirty();
}
protected void RemoveFilter()
{
Filters = null;
MarkFilesDirty();
}
public async ValueTask DisposeAsync()
@@ -158,6 +164,7 @@ namespace Lantean.QBTMud.Components
protected void SearchTextChanged(string value)
{
SearchText = value;
MarkFilesDirty();
}
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
@@ -198,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;
@@ -214,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);
}
}
}
}
@@ -247,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)
@@ -257,6 +273,8 @@ namespace Lantean.QBTMud.Components
{
ExpandedNodes.Clear();
}
MarkFilesDirty();
}
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
@@ -321,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)
@@ -344,6 +364,7 @@ namespace Lantean.QBTMud.Components
ExpandedNodes.Add(contentItem.Name);
}
MarkFilesDirty();
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes);
}
@@ -369,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)
{
var childLevel = folder.Level + 1;
var prefix = string.Concat(folder.Name, Extensions.DirectorySeparator);
foreach (var item in FileList!.Values.Where(f => f.Level == childLevel && f.Name.StartsWith(prefix, StringComparison.Ordinal)).OrderByDirection(_sortDirection, GetSortSelector()))
{
if (item.IsFolder)
{
var descendants = GetChildren(item);
// 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)
@@ -430,37 +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 descendants = GetChildren(item);
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()

View File

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

View File

@@ -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">
@@ -65,5 +66,6 @@
@DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")")
</MudText>
</MudAppBar>
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@@ -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);
@@ -167,6 +203,76 @@ namespace Lantean.QBTMud.Layout
return (Icons.Material.Outlined.SignalWifi4Bar, Color.Success);
}
private void OnCategoryChanged(string category)
{
if (Category == category)
{
return;
}
Category = category;
MarkTorrentsDirty();
}
private void OnStatusChanged(Status status)
{
if (Status == status)
{
return;
}
Status = status;
MarkTorrentsDirty();
}
private void OnTagChanged(string tag)
{
if (Tag == tag)
{
return;
}
Tag = tag;
MarkTorrentsDirty();
}
private void OnTrackerChanged(string tracker)
{
if (Tracker == tracker)
{
return;
}
Tracker = tracker;
MarkTorrentsDirty();
}
private void OnSearchTermChanged(string term)
{
if (SearchText == term)
{
return;
}
SearchText = term;
MarkTorrentsDirty();
}
private void MarkTorrentsDirty()
{
_torrentsDirty = true;
IncrementTorrentsVersion();
}
private void IncrementTorrentsVersion()
{
unchecked
{
_torrentsVersion++;
}
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
@@ -188,4 +294,4 @@ namespace Lantean.QBTMud.Layout
GC.SuppressFinalize(this);
}
}
}
}

View File

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

View File

@@ -1,5 +1,6 @@
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Helpers;
using Lantean.QBTMud.Models;
using System.Linq;
namespace Lantean.QBTMud.Services
{
@@ -142,14 +143,24 @@ namespace Lantean.QBTMud.Services
serverState.WriteCacheOverload.GetValueOrDefault());
}
public void MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList)
public bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged)
{
filterChanged = false;
var dataChanged = false;
if (mainData.CategoriesRemoved is not null)
{
foreach (var category in mainData.CategoriesRemoved)
{
torrentList.Categories.Remove(category);
torrentList.CategoriesState.Remove(category);
if (torrentList.Categories.Remove(category))
{
dataChanged = true;
filterChanged = true;
}
if (torrentList.CategoriesState.Remove(category))
{
filterChanged = true;
}
}
}
@@ -157,8 +168,15 @@ namespace Lantean.QBTMud.Services
{
foreach (var tag in mainData.TagsRemoved)
{
torrentList.Tags.Remove(tag);
torrentList.TagState.Remove(tag);
if (torrentList.Tags.Remove(tag))
{
dataChanged = true;
filterChanged = true;
}
if (torrentList.TagState.Remove(tag))
{
filterChanged = true;
}
}
}
@@ -166,8 +184,15 @@ namespace Lantean.QBTMud.Services
{
foreach (var tracker in mainData.TrackersRemoved)
{
torrentList.Trackers.Remove(tracker);
torrentList.TrackersState.Remove(tracker);
if (torrentList.Trackers.Remove(tracker))
{
dataChanged = true;
filterChanged = true;
}
if (torrentList.TrackersState.Remove(tracker))
{
filterChanged = true;
}
}
}
@@ -175,8 +200,12 @@ namespace Lantean.QBTMud.Services
{
foreach (var hash in mainData.TorrentsRemoved)
{
RemoveTorrentFromStates(torrentList, hash);
torrentList.Torrents.Remove(hash);
if (torrentList.Torrents.Remove(hash))
{
RemoveTorrentFromStates(torrentList, hash);
dataChanged = true;
filterChanged = true;
}
}
}
@@ -188,10 +217,12 @@ namespace Lantean.QBTMud.Services
{
var newCategory = CreateCategory(category);
torrentList.Categories.Add(name, newCategory);
dataChanged = true;
filterChanged = true;
}
else
else if (UpdateCategory(existingCategory, category))
{
UpdateCategory(existingCategory, category);
dataChanged = true;
}
}
}
@@ -200,7 +231,11 @@ namespace Lantean.QBTMud.Services
{
foreach (var tag in mainData.Tags)
{
torrentList.Tags.Add(tag);
if (torrentList.Tags.Add(tag))
{
dataChanged = true;
filterChanged = true;
}
}
}
@@ -208,13 +243,16 @@ namespace Lantean.QBTMud.Services
{
foreach (var (url, hashes) in mainData.Trackers)
{
if (!torrentList.Trackers.TryGetValue(url, out _))
if (!torrentList.Trackers.TryGetValue(url, out var existingHashes))
{
torrentList.Trackers.Add(url, hashes);
dataChanged = true;
filterChanged = true;
}
else
else if (!existingHashes.SequenceEqual(hashes))
{
torrentList.Trackers[url] = hashes;
dataChanged = true;
}
}
}
@@ -228,19 +266,34 @@ namespace Lantean.QBTMud.Services
var newTorrent = CreateTorrent(hash, torrent);
torrentList.Torrents.Add(hash, newTorrent);
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
dataChanged = true;
filterChanged = true;
}
else
{
UpdateTorrentStates(torrentList, hash);
UpdateTorrent(existingTorrent, torrent);
var updateResult = UpdateTorrent(existingTorrent, torrent);
if (updateResult.FilterChanged)
{
UpdateTorrentStates(torrentList, hash);
filterChanged = true;
}
if (updateResult.DataChanged)
{
dataChanged = true;
}
}
}
}
if (mainData.ServerState is not null)
{
UpdateServerState(torrentList.ServerState, mainData.ServerState);
if (UpdateServerState(torrentList.ServerState, mainData.ServerState))
{
dataChanged = true;
}
}
return dataChanged;
}
private static void AddTorrentToStates(MainData torrentList, string hash, int version)
@@ -403,33 +456,161 @@ namespace Lantean.QBTMud.Services
}
}
private static void UpdateServerState(ServerState existingServerState, QBitTorrentClient.Models.ServerState serverState)
private static bool UpdateServerState(ServerState existingServerState, QBitTorrentClient.Models.ServerState serverState)
{
existingServerState.AllTimeDownloaded = serverState.AllTimeDownloaded ?? existingServerState.AllTimeDownloaded;
existingServerState.AllTimeUploaded = serverState.AllTimeUploaded ?? existingServerState.AllTimeUploaded;
existingServerState.AverageTimeQueue = serverState.AverageTimeQueue ?? existingServerState.AverageTimeQueue;
existingServerState.ConnectionStatus = serverState.ConnectionStatus ?? existingServerState.ConnectionStatus;
existingServerState.DHTNodes = serverState.DHTNodes ?? existingServerState.DHTNodes;
existingServerState.DownloadInfoData = serverState.DownloadInfoData ?? existingServerState.DownloadInfoData;
existingServerState.DownloadInfoSpeed = serverState.DownloadInfoSpeed ?? existingServerState.DownloadInfoSpeed;
existingServerState.DownloadRateLimit = serverState.DownloadRateLimit ?? existingServerState.DownloadRateLimit;
existingServerState.FreeSpaceOnDisk = serverState.FreeSpaceOnDisk ?? existingServerState.FreeSpaceOnDisk;
existingServerState.GlobalRatio = serverState.GlobalRatio ?? existingServerState.GlobalRatio;
existingServerState.QueuedIOJobs = serverState.QueuedIOJobs ?? existingServerState.QueuedIOJobs;
existingServerState.Queuing = serverState.Queuing ?? existingServerState.Queuing;
existingServerState.ReadCacheHits = serverState.ReadCacheHits ?? existingServerState.ReadCacheHits;
existingServerState.ReadCacheOverload = serverState.ReadCacheOverload ?? existingServerState.ReadCacheOverload;
existingServerState.RefreshInterval = serverState.RefreshInterval ?? existingServerState.RefreshInterval;
existingServerState.TotalBuffersSize = serverState.TotalBuffersSize ?? existingServerState.TotalBuffersSize;
existingServerState.TotalPeerConnections = serverState.TotalPeerConnections ?? existingServerState.TotalPeerConnections;
existingServerState.TotalQueuedSize = serverState.TotalQueuedSize ?? existingServerState.TotalQueuedSize;
existingServerState.TotalWastedSession = serverState.TotalWastedSession ?? existingServerState.TotalWastedSession;
existingServerState.UploadInfoData = serverState.UploadInfoData ?? existingServerState.UploadInfoData;
existingServerState.UploadInfoSpeed = serverState.UploadInfoSpeed ?? existingServerState.UploadInfoSpeed;
existingServerState.UploadRateLimit = serverState.UploadRateLimit ?? existingServerState.UploadRateLimit;
existingServerState.UseAltSpeedLimits = serverState.UseAltSpeedLimits ?? existingServerState.UseAltSpeedLimits;
existingServerState.UseSubcategories = serverState.UseSubcategories ?? existingServerState.UseSubcategories;
existingServerState.WriteCacheOverload = serverState.WriteCacheOverload ?? existingServerState.WriteCacheOverload;
var changed = false;
if (serverState.AllTimeDownloaded.HasValue && existingServerState.AllTimeDownloaded != serverState.AllTimeDownloaded.Value)
{
existingServerState.AllTimeDownloaded = serverState.AllTimeDownloaded.Value;
changed = true;
}
if (serverState.AllTimeUploaded.HasValue && existingServerState.AllTimeUploaded != serverState.AllTimeUploaded.Value)
{
existingServerState.AllTimeUploaded = serverState.AllTimeUploaded.Value;
changed = true;
}
if (serverState.AverageTimeQueue.HasValue && existingServerState.AverageTimeQueue != serverState.AverageTimeQueue.Value)
{
existingServerState.AverageTimeQueue = serverState.AverageTimeQueue.Value;
changed = true;
}
if (serverState.ConnectionStatus is not null && existingServerState.ConnectionStatus != serverState.ConnectionStatus)
{
existingServerState.ConnectionStatus = serverState.ConnectionStatus;
changed = true;
}
if (serverState.DHTNodes.HasValue && existingServerState.DHTNodes != serverState.DHTNodes.Value)
{
existingServerState.DHTNodes = serverState.DHTNodes.Value;
changed = true;
}
if (serverState.DownloadInfoData.HasValue && existingServerState.DownloadInfoData != serverState.DownloadInfoData.Value)
{
existingServerState.DownloadInfoData = serverState.DownloadInfoData.Value;
changed = true;
}
if (serverState.DownloadInfoSpeed.HasValue && existingServerState.DownloadInfoSpeed != serverState.DownloadInfoSpeed.Value)
{
existingServerState.DownloadInfoSpeed = serverState.DownloadInfoSpeed.Value;
changed = true;
}
if (serverState.DownloadRateLimit.HasValue && existingServerState.DownloadRateLimit != serverState.DownloadRateLimit.Value)
{
existingServerState.DownloadRateLimit = serverState.DownloadRateLimit.Value;
changed = true;
}
if (serverState.FreeSpaceOnDisk.HasValue && existingServerState.FreeSpaceOnDisk != serverState.FreeSpaceOnDisk.Value)
{
existingServerState.FreeSpaceOnDisk = serverState.FreeSpaceOnDisk.Value;
changed = true;
}
if (serverState.GlobalRatio.HasValue && existingServerState.GlobalRatio != serverState.GlobalRatio.Value)
{
existingServerState.GlobalRatio = serverState.GlobalRatio.Value;
changed = true;
}
if (serverState.QueuedIOJobs.HasValue && existingServerState.QueuedIOJobs != serverState.QueuedIOJobs.Value)
{
existingServerState.QueuedIOJobs = serverState.QueuedIOJobs.Value;
changed = true;
}
if (serverState.Queuing.HasValue && existingServerState.Queuing != serverState.Queuing.Value)
{
existingServerState.Queuing = serverState.Queuing.Value;
changed = true;
}
if (serverState.ReadCacheHits.HasValue && existingServerState.ReadCacheHits != serverState.ReadCacheHits.Value)
{
existingServerState.ReadCacheHits = serverState.ReadCacheHits.Value;
changed = true;
}
if (serverState.ReadCacheOverload.HasValue && existingServerState.ReadCacheOverload != serverState.ReadCacheOverload.Value)
{
existingServerState.ReadCacheOverload = serverState.ReadCacheOverload.Value;
changed = true;
}
if (serverState.RefreshInterval.HasValue && existingServerState.RefreshInterval != serverState.RefreshInterval.Value)
{
existingServerState.RefreshInterval = serverState.RefreshInterval.Value;
changed = true;
}
if (serverState.TotalBuffersSize.HasValue && existingServerState.TotalBuffersSize != serverState.TotalBuffersSize.Value)
{
existingServerState.TotalBuffersSize = serverState.TotalBuffersSize.Value;
changed = true;
}
if (serverState.TotalPeerConnections.HasValue && existingServerState.TotalPeerConnections != serverState.TotalPeerConnections.Value)
{
existingServerState.TotalPeerConnections = serverState.TotalPeerConnections.Value;
changed = true;
}
if (serverState.TotalQueuedSize.HasValue && existingServerState.TotalQueuedSize != serverState.TotalQueuedSize.Value)
{
existingServerState.TotalQueuedSize = serverState.TotalQueuedSize.Value;
changed = true;
}
if (serverState.TotalWastedSession.HasValue && existingServerState.TotalWastedSession != serverState.TotalWastedSession.Value)
{
existingServerState.TotalWastedSession = serverState.TotalWastedSession.Value;
changed = true;
}
if (serverState.UploadInfoData.HasValue && existingServerState.UploadInfoData != serverState.UploadInfoData.Value)
{
existingServerState.UploadInfoData = serverState.UploadInfoData.Value;
changed = true;
}
if (serverState.UploadInfoSpeed.HasValue && existingServerState.UploadInfoSpeed != serverState.UploadInfoSpeed.Value)
{
existingServerState.UploadInfoSpeed = serverState.UploadInfoSpeed.Value;
changed = true;
}
if (serverState.UploadRateLimit.HasValue && existingServerState.UploadRateLimit != serverState.UploadRateLimit.Value)
{
existingServerState.UploadRateLimit = serverState.UploadRateLimit.Value;
changed = true;
}
if (serverState.UseAltSpeedLimits.HasValue && existingServerState.UseAltSpeedLimits != serverState.UseAltSpeedLimits.Value)
{
existingServerState.UseAltSpeedLimits = serverState.UseAltSpeedLimits.Value;
changed = true;
}
if (serverState.UseSubcategories.HasValue && existingServerState.UseSubcategories != serverState.UseSubcategories.Value)
{
existingServerState.UseSubcategories = serverState.UseSubcategories.Value;
changed = true;
}
if (serverState.WriteCacheOverload.HasValue && existingServerState.WriteCacheOverload != serverState.WriteCacheOverload.Value)
{
existingServerState.WriteCacheOverload = serverState.WriteCacheOverload.Value;
changed = true;
}
return changed;
}
public void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList)
@@ -561,66 +742,344 @@ namespace Lantean.QBTMud.Services
torrent.MaxInactiveSeedingTime.GetValueOrDefault());
}
private static void UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category)
private static bool UpdateCategory(Category existingCategory, QBitTorrentClient.Models.Category category)
{
existingCategory.SavePath = category.SavePath ?? existingCategory.SavePath;
if (category.SavePath is not null && existingCategory.SavePath != category.SavePath)
{
existingCategory.SavePath = category.SavePath;
return true;
}
return false;
}
private static void UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent)
private readonly struct TorrentUpdateResult
{
existingTorrent.AddedOn = torrent.AddedOn ?? existingTorrent.AddedOn;
existingTorrent.AmountLeft = torrent.AmountLeft ?? existingTorrent.AmountLeft;
existingTorrent.AutomaticTorrentManagement = torrent.AutomaticTorrentManagement ?? existingTorrent.AutomaticTorrentManagement;
existingTorrent.Availability = torrent.Availability ?? existingTorrent.Availability;
existingTorrent.Category = torrent.Category ?? existingTorrent.Category;
existingTorrent.Completed = torrent.Completed ?? existingTorrent.Completed;
existingTorrent.CompletionOn = torrent.CompletionOn ?? existingTorrent.CompletionOn;
existingTorrent.ContentPath = torrent.ContentPath ?? existingTorrent.ContentPath;
existingTorrent.Downloaded = torrent.Downloaded ?? existingTorrent.Downloaded;
existingTorrent.DownloadedSession = torrent.DownloadedSession ?? existingTorrent.DownloadedSession;
existingTorrent.DownloadLimit = torrent.DownloadLimit ?? existingTorrent.DownloadLimit;
existingTorrent.DownloadSpeed = torrent.DownloadSpeed ?? existingTorrent.DownloadSpeed;
existingTorrent.EstimatedTimeOfArrival = torrent.EstimatedTimeOfArrival ?? existingTorrent.EstimatedTimeOfArrival;
existingTorrent.FirstLastPiecePriority = torrent.FirstLastPiecePriority ?? existingTorrent.FirstLastPiecePriority;
existingTorrent.ForceStart = torrent.ForceStart ?? existingTorrent.ForceStart;
existingTorrent.InfoHashV1 = torrent.InfoHashV1 ?? existingTorrent.InfoHashV1;
existingTorrent.InfoHashV2 = torrent.InfoHashV2 ?? existingTorrent.InfoHashV2;
existingTorrent.LastActivity = torrent.LastActivity ?? existingTorrent.LastActivity;
existingTorrent.MagnetUri = torrent.MagnetUri ?? existingTorrent.MagnetUri;
existingTorrent.MaxRatio = torrent.MaxRatio ?? existingTorrent.MaxRatio;
existingTorrent.MaxSeedingTime = torrent.MaxSeedingTime ?? existingTorrent.MaxSeedingTime;
existingTorrent.Name = torrent.Name ?? existingTorrent.Name;
existingTorrent.NumberComplete = torrent.NumberComplete ?? existingTorrent.NumberComplete;
existingTorrent.NumberIncomplete = torrent.NumberIncomplete ?? existingTorrent.NumberIncomplete;
existingTorrent.NumberLeeches = torrent.NumberLeeches ?? existingTorrent.NumberLeeches;
existingTorrent.NumberSeeds = torrent.NumberSeeds ?? existingTorrent.NumberSeeds;
existingTorrent.Priority = torrent.Priority ?? existingTorrent.Priority;
existingTorrent.Progress = torrent.Progress ?? existingTorrent.Progress;
existingTorrent.Ratio = torrent.Ratio ?? existingTorrent.Ratio;
existingTorrent.RatioLimit = torrent.RatioLimit ?? existingTorrent.RatioLimit;
existingTorrent.SavePath = torrent.SavePath ?? existingTorrent.SavePath;
existingTorrent.SeedingTime = torrent.SeedingTime ?? existingTorrent.SeedingTime;
existingTorrent.SeedingTimeLimit = torrent.SeedingTimeLimit ?? existingTorrent.SeedingTimeLimit;
existingTorrent.SeenComplete = torrent.SeenComplete ?? existingTorrent.SeenComplete;
existingTorrent.SequentialDownload = torrent.SequentialDownload ?? existingTorrent.SequentialDownload;
existingTorrent.Size = torrent.Size ?? existingTorrent.Size;
existingTorrent.State = torrent.State ?? existingTorrent.State;
existingTorrent.SuperSeeding = torrent.SuperSeeding ?? existingTorrent.SuperSeeding;
public TorrentUpdateResult(bool dataChanged, bool filterChanged)
{
DataChanged = dataChanged;
FilterChanged = filterChanged;
}
public bool DataChanged { get; }
public bool FilterChanged { get; }
}
private static TorrentUpdateResult UpdateTorrent(Torrent existingTorrent, QBitTorrentClient.Models.Torrent torrent)
{
var dataChanged = false;
var filterChanged = false;
if (torrent.AddedOn.HasValue && existingTorrent.AddedOn != torrent.AddedOn.Value)
{
existingTorrent.AddedOn = torrent.AddedOn.Value;
dataChanged = true;
}
if (torrent.AmountLeft.HasValue && existingTorrent.AmountLeft != torrent.AmountLeft.Value)
{
existingTorrent.AmountLeft = torrent.AmountLeft.Value;
dataChanged = true;
}
if (torrent.AutomaticTorrentManagement.HasValue && existingTorrent.AutomaticTorrentManagement != torrent.AutomaticTorrentManagement.Value)
{
existingTorrent.AutomaticTorrentManagement = torrent.AutomaticTorrentManagement.Value;
dataChanged = true;
}
if (torrent.Availability.HasValue && existingTorrent.Availability != torrent.Availability.Value)
{
existingTorrent.Availability = torrent.Availability.Value;
dataChanged = true;
}
if (torrent.Category is not null && existingTorrent.Category != torrent.Category)
{
existingTorrent.Category = torrent.Category;
dataChanged = true;
filterChanged = true;
}
if (torrent.Completed.HasValue && existingTorrent.Completed != torrent.Completed.Value)
{
existingTorrent.Completed = torrent.Completed.Value;
dataChanged = true;
}
if (torrent.CompletionOn.HasValue && existingTorrent.CompletionOn != torrent.CompletionOn.Value)
{
existingTorrent.CompletionOn = torrent.CompletionOn.Value;
dataChanged = true;
}
if (torrent.ContentPath is not null && existingTorrent.ContentPath != torrent.ContentPath)
{
existingTorrent.ContentPath = torrent.ContentPath;
dataChanged = true;
}
if (torrent.Downloaded.HasValue && existingTorrent.Downloaded != torrent.Downloaded.Value)
{
existingTorrent.Downloaded = torrent.Downloaded.Value;
dataChanged = true;
}
if (torrent.DownloadedSession.HasValue && existingTorrent.DownloadedSession != torrent.DownloadedSession.Value)
{
existingTorrent.DownloadedSession = torrent.DownloadedSession.Value;
dataChanged = true;
}
if (torrent.DownloadLimit.HasValue && existingTorrent.DownloadLimit != torrent.DownloadLimit.Value)
{
existingTorrent.DownloadLimit = torrent.DownloadLimit.Value;
dataChanged = true;
}
if (torrent.DownloadSpeed.HasValue && existingTorrent.DownloadSpeed != torrent.DownloadSpeed.Value)
{
existingTorrent.DownloadSpeed = torrent.DownloadSpeed.Value;
dataChanged = true;
}
if (torrent.EstimatedTimeOfArrival.HasValue && existingTorrent.EstimatedTimeOfArrival != torrent.EstimatedTimeOfArrival.Value)
{
existingTorrent.EstimatedTimeOfArrival = torrent.EstimatedTimeOfArrival.Value;
dataChanged = true;
}
if (torrent.FirstLastPiecePriority.HasValue && existingTorrent.FirstLastPiecePriority != torrent.FirstLastPiecePriority.Value)
{
existingTorrent.FirstLastPiecePriority = torrent.FirstLastPiecePriority.Value;
dataChanged = true;
}
if (torrent.ForceStart.HasValue && existingTorrent.ForceStart != torrent.ForceStart.Value)
{
existingTorrent.ForceStart = torrent.ForceStart.Value;
dataChanged = true;
}
if (torrent.InfoHashV1 is not null && existingTorrent.InfoHashV1 != torrent.InfoHashV1)
{
existingTorrent.InfoHashV1 = torrent.InfoHashV1;
dataChanged = true;
}
if (torrent.InfoHashV2 is not null && existingTorrent.InfoHashV2 != torrent.InfoHashV2)
{
existingTorrent.InfoHashV2 = torrent.InfoHashV2;
dataChanged = true;
}
if (torrent.LastActivity.HasValue && existingTorrent.LastActivity != torrent.LastActivity.Value)
{
existingTorrent.LastActivity = torrent.LastActivity.Value;
dataChanged = true;
}
if (torrent.MagnetUri is not null && existingTorrent.MagnetUri != torrent.MagnetUri)
{
existingTorrent.MagnetUri = torrent.MagnetUri;
dataChanged = true;
}
if (torrent.MaxRatio.HasValue && existingTorrent.MaxRatio != torrent.MaxRatio.Value)
{
existingTorrent.MaxRatio = torrent.MaxRatio.Value;
dataChanged = true;
}
if (torrent.MaxSeedingTime.HasValue && existingTorrent.MaxSeedingTime != torrent.MaxSeedingTime.Value)
{
existingTorrent.MaxSeedingTime = torrent.MaxSeedingTime.Value;
dataChanged = true;
}
if (torrent.Name is not null && existingTorrent.Name != torrent.Name)
{
existingTorrent.Name = torrent.Name;
dataChanged = true;
filterChanged = true;
}
if (torrent.NumberComplete.HasValue && existingTorrent.NumberComplete != torrent.NumberComplete.Value)
{
existingTorrent.NumberComplete = torrent.NumberComplete.Value;
dataChanged = true;
}
if (torrent.NumberIncomplete.HasValue && existingTorrent.NumberIncomplete != torrent.NumberIncomplete.Value)
{
existingTorrent.NumberIncomplete = torrent.NumberIncomplete.Value;
dataChanged = true;
}
if (torrent.NumberLeeches.HasValue && existingTorrent.NumberLeeches != torrent.NumberLeeches.Value)
{
existingTorrent.NumberLeeches = torrent.NumberLeeches.Value;
dataChanged = true;
}
if (torrent.NumberSeeds.HasValue && existingTorrent.NumberSeeds != torrent.NumberSeeds.Value)
{
existingTorrent.NumberSeeds = torrent.NumberSeeds.Value;
dataChanged = true;
}
if (torrent.Priority.HasValue && existingTorrent.Priority != torrent.Priority.Value)
{
existingTorrent.Priority = torrent.Priority.Value;
dataChanged = true;
}
if (torrent.Progress.HasValue && existingTorrent.Progress != torrent.Progress.Value)
{
existingTorrent.Progress = torrent.Progress.Value;
dataChanged = true;
}
if (torrent.Ratio.HasValue && existingTorrent.Ratio != torrent.Ratio.Value)
{
existingTorrent.Ratio = torrent.Ratio.Value;
dataChanged = true;
}
if (torrent.RatioLimit.HasValue && existingTorrent.RatioLimit != torrent.RatioLimit.Value)
{
existingTorrent.RatioLimit = torrent.RatioLimit.Value;
dataChanged = true;
}
if (torrent.SavePath is not null && existingTorrent.SavePath != torrent.SavePath)
{
existingTorrent.SavePath = torrent.SavePath;
dataChanged = true;
}
if (torrent.SeedingTime.HasValue && existingTorrent.SeedingTime != torrent.SeedingTime.Value)
{
existingTorrent.SeedingTime = torrent.SeedingTime.Value;
dataChanged = true;
}
if (torrent.SeedingTimeLimit.HasValue && existingTorrent.SeedingTimeLimit != torrent.SeedingTimeLimit.Value)
{
existingTorrent.SeedingTimeLimit = torrent.SeedingTimeLimit.Value;
dataChanged = true;
}
if (torrent.SeenComplete.HasValue && existingTorrent.SeenComplete != torrent.SeenComplete.Value)
{
existingTorrent.SeenComplete = torrent.SeenComplete.Value;
dataChanged = true;
}
if (torrent.SequentialDownload.HasValue && existingTorrent.SequentialDownload != torrent.SequentialDownload.Value)
{
existingTorrent.SequentialDownload = torrent.SequentialDownload.Value;
dataChanged = true;
}
if (torrent.Size.HasValue && existingTorrent.Size != torrent.Size.Value)
{
existingTorrent.Size = torrent.Size.Value;
dataChanged = true;
}
if (torrent.State is not null && existingTorrent.State != torrent.State)
{
existingTorrent.State = torrent.State;
dataChanged = true;
filterChanged = true;
}
if (torrent.SuperSeeding.HasValue && existingTorrent.SuperSeeding != torrent.SuperSeeding.Value)
{
existingTorrent.SuperSeeding = torrent.SuperSeeding.Value;
dataChanged = true;
}
if (torrent.Tags is not null)
{
existingTorrent.Tags.Clear();
existingTorrent.Tags.AddRange(torrent.Tags);
if (!existingTorrent.Tags.SequenceEqual(torrent.Tags))
{
existingTorrent.Tags.Clear();
existingTorrent.Tags.AddRange(torrent.Tags);
dataChanged = true;
filterChanged = true;
}
}
existingTorrent.TimeActive = torrent.TimeActive ?? existingTorrent.TimeActive;
existingTorrent.TotalSize = torrent.TotalSize ?? existingTorrent.TotalSize;
existingTorrent.Tracker = torrent.Tracker ?? existingTorrent.Tracker;
existingTorrent.UploadLimit = torrent.UploadLimit ?? existingTorrent.UploadLimit;
existingTorrent.Uploaded = torrent.Uploaded ?? existingTorrent.Uploaded;
existingTorrent.UploadedSession = torrent.UploadedSession ?? existingTorrent.UploadedSession;
existingTorrent.UploadSpeed = torrent.UploadSpeed ?? existingTorrent.UploadSpeed;
existingTorrent.Reannounce = torrent.Reannounce ?? existingTorrent.Reannounce;
existingTorrent.InactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit ?? existingTorrent.InactiveSeedingTimeLimit;
existingTorrent.MaxInactiveSeedingTime = torrent.MaxInactiveSeedingTime ?? existingTorrent.MaxInactiveSeedingTime;
if (torrent.TimeActive.HasValue && existingTorrent.TimeActive != torrent.TimeActive.Value)
{
existingTorrent.TimeActive = torrent.TimeActive.Value;
dataChanged = true;
}
if (torrent.TotalSize.HasValue && existingTorrent.TotalSize != torrent.TotalSize.Value)
{
existingTorrent.TotalSize = torrent.TotalSize.Value;
dataChanged = true;
}
if (torrent.Tracker is not null && existingTorrent.Tracker != torrent.Tracker)
{
existingTorrent.Tracker = torrent.Tracker;
dataChanged = true;
filterChanged = true;
}
if (torrent.UploadLimit.HasValue && existingTorrent.UploadLimit != torrent.UploadLimit.Value)
{
existingTorrent.UploadLimit = torrent.UploadLimit.Value;
dataChanged = true;
}
if (torrent.Uploaded.HasValue && existingTorrent.Uploaded != torrent.Uploaded.Value)
{
existingTorrent.Uploaded = torrent.Uploaded.Value;
dataChanged = true;
}
if (torrent.UploadedSession.HasValue && existingTorrent.UploadedSession != torrent.UploadedSession.Value)
{
existingTorrent.UploadedSession = torrent.UploadedSession.Value;
dataChanged = true;
}
var previousUploadSpeed = existingTorrent.UploadSpeed;
if (torrent.UploadSpeed.HasValue && previousUploadSpeed != torrent.UploadSpeed.Value)
{
existingTorrent.UploadSpeed = torrent.UploadSpeed.Value;
dataChanged = true;
if ((previousUploadSpeed > 0) != (torrent.UploadSpeed.Value > 0))
{
filterChanged = true;
}
}
if (torrent.Reannounce.HasValue && existingTorrent.Reannounce != torrent.Reannounce.Value)
{
existingTorrent.Reannounce = torrent.Reannounce.Value;
dataChanged = true;
}
if (torrent.InactiveSeedingTimeLimit.HasValue && existingTorrent.InactiveSeedingTimeLimit != torrent.InactiveSeedingTimeLimit.Value)
{
existingTorrent.InactiveSeedingTimeLimit = torrent.InactiveSeedingTimeLimit.Value;
dataChanged = true;
}
if (torrent.MaxInactiveSeedingTime.HasValue && existingTorrent.MaxInactiveSeedingTime != torrent.MaxInactiveSeedingTime.Value)
{
existingTorrent.MaxInactiveSeedingTime = torrent.MaxInactiveSeedingTime.Value;
dataChanged = true;
}
return new TorrentUpdateResult(dataChanged, filterChanged);
}
public Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files)
@@ -745,6 +1204,111 @@ namespace Lantean.QBTMud.Services
return result;
}
private static bool UpdateContentItem(ContentItem destination, ContentItem source)
{
const float floatTolerance = 0.0001f;
var changed = false;
if (destination.Priority != source.Priority)
{
destination.Priority = source.Priority;
changed = true;
}
if (System.Math.Abs(destination.Progress - source.Progress) > floatTolerance)
{
destination.Progress = source.Progress;
changed = true;
}
if (destination.Size != source.Size)
{
destination.Size = source.Size;
changed = true;
}
if (System.Math.Abs(destination.Availability - source.Availability) > floatTolerance)
{
destination.Availability = source.Availability;
changed = true;
}
return changed;
}
private struct DirectoryAccumulator
{
public long TotalSize { get; private set; }
private long _activeSize;
private double _progressSum;
private double _availabilitySum;
private Priority? _priority;
private bool _mixedPriority;
public void Add(Priority priority, float progress, long size, float availability)
{
TotalSize += size;
if (priority != Priority.DoNotDownload)
{
_activeSize += size;
_progressSum += progress * size;
_availabilitySum += availability * size;
}
if (!_priority.HasValue)
{
_priority = priority;
}
else if (_priority.Value != priority)
{
_mixedPriority = true;
}
}
public Priority ResolvePriority()
{
if (_mixedPriority)
{
return Priority.Mixed;
}
return _priority ?? Priority.Normal;
}
public float ResolveProgress()
{
if (_activeSize == 0 || TotalSize == 0)
{
return 0f;
}
var value = _progressSum / _activeSize;
if (value < 0)
{
return 0f;
}
if (value > 1)
{
return 1f;
}
return (float)value;
}
public float ResolveAvailability()
{
if (_activeSize == 0 || TotalSize == 0)
{
return 0f;
}
return (float)(_availabilitySum / _activeSize);
}
}
private sealed class ContentTreeNode
{
public ContentTreeNode(ContentItem? item, ContentTreeNode? parent)
@@ -1180,24 +1744,120 @@ namespace Lantean.QBTMud.Services
return original;
}
public void MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents)
public bool MergeContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files, Dictionary<string, ContentItem> contents)
{
var contentsList = CreateContentsList(files);
foreach (var (key, value) in contentsList)
if (files.Count == 0)
{
if (contents.TryGetValue(key, out var content))
if (contents.Count == 0)
{
content.Availability = value.Availability;
content.Priority = value.Priority;
content.Progress = value.Progress;
content.Size = value.Size;
return false;
}
contents.Clear();
return true;
}
var hasChanges = false;
var seenPaths = new HashSet<string>(files.Count * 2);
var directoryAccumulators = new Dictionary<string, DirectoryAccumulator>();
var minExistingIndex = contents.Count == 0
? int.MaxValue
: contents.Values.Min(c => c.Index);
var minFileIndex = files.Min(f => f.Index);
var nextFolderIndex = System.Math.Min(minExistingIndex, minFileIndex) - 1;
foreach (var file in files)
{
var priority = (Priority)(int)file.Priority;
var pathSegments = file.Name.Split(Extensions.DirectorySeparator);
var level = pathSegments.Length - 1;
var displayName = pathSegments[^1];
var filePath = file.Name;
seenPaths.Add(filePath);
if (contents.TryGetValue(filePath, out var existingFile))
{
var updatedFile = new ContentItem(filePath, displayName, file.Index, priority, file.Progress, file.Size, file.Availability, false, level);
if (UpdateContentItem(existingFile, updatedFile))
{
hasChanges = true;
}
}
else
{
contents[key] = value;
var newFile = new ContentItem(filePath, displayName, file.Index, priority, file.Progress, file.Size, file.Availability, false, level);
contents[filePath] = newFile;
hasChanges = true;
}
string directoryPath = string.Empty;
for (var i = 0; i < level; i++)
{
var segment = pathSegments[i];
if (segment == ".unwanted")
{
continue;
}
directoryPath = string.IsNullOrEmpty(directoryPath)
? segment
: string.Concat(directoryPath, Extensions.DirectorySeparator, segment);
seenPaths.Add(directoryPath);
if (!contents.TryGetValue(directoryPath, out var directoryItem))
{
var newDirectory = new ContentItem(directoryPath, segment, nextFolderIndex--, Priority.Normal, 0, 0, 0, true, i);
contents[directoryPath] = newDirectory;
hasChanges = true;
}
if (!directoryAccumulators.TryGetValue(directoryPath, out var accumulator))
{
accumulator = new DirectoryAccumulator();
}
accumulator.Add(priority, file.Progress, file.Size, file.Availability);
directoryAccumulators[directoryPath] = accumulator;
}
}
var keysToRemove = contents.Keys.Where(key => !seenPaths.Contains(key)).ToList();
if (keysToRemove.Count != 0)
{
hasChanges = true;
foreach (var key in keysToRemove)
{
contents.Remove(key);
}
}
foreach (var (directoryPath, accumulator) in directoryAccumulators)
{
if (!contents.TryGetValue(directoryPath, out var directoryItem))
{
continue;
}
var updatedDirectory = new ContentItem(
directoryPath,
directoryItem.DisplayName,
directoryItem.Index,
accumulator.ResolvePriority(),
accumulator.ResolveProgress(),
accumulator.TotalSize,
accumulator.ResolveAvailability(),
true,
directoryItem.Level);
if (UpdateContentItem(directoryItem, updatedDirectory))
{
hasChanges = true;
}
}
return hasChanges;
}
public RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems)

View File

@@ -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,7 +16,7 @@ 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);

5
global.json Normal file
View File

@@ -0,0 +1,5 @@
{
"sdk": {
"version": "9.0.306"
}
}