Improve file list performance.

This commit is contained in:
ahjephson
2025-10-19 14:19:21 +01:00
parent bb524450f0
commit a9a8a4eba8
4 changed files with 437 additions and 87 deletions

View File

@@ -21,6 +21,9 @@ namespace Lantean.QBTMud.Components
private readonly CancellationTokenSource _timerCancellationToken = new(); private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue; 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 List<PropertyFilterDefinition<ContentItem>>? _filterDefinitions;
private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = []; private readonly Dictionary<string, RenderFragment<RowContext<ContentItem>>> _columnRenderFragments = [];
@@ -103,6 +106,7 @@ namespace Lantean.QBTMud.Components
if (_filterDefinitions is null) if (_filterDefinitions is null)
{ {
Filters = null; Filters = null;
MarkFilesDirty();
return; return;
} }
@@ -114,11 +118,13 @@ namespace Lantean.QBTMud.Components
} }
Filters = filters; Filters = filters;
MarkFilesDirty();
} }
protected void RemoveFilter() protected void RemoveFilter()
{ {
Filters = null; Filters = null;
MarkFilesDirty();
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
@@ -158,6 +164,7 @@ namespace Lantean.QBTMud.Components
protected void SearchTextChanged(string value) protected void SearchTextChanged(string value)
{ {
SearchText = value; SearchText = value;
MarkFilesDirty();
} }
protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs) protected Task TableDataContextMenu(TableDataContextMenuEventArgs<ContentItem> eventArgs)
@@ -198,6 +205,7 @@ namespace Lantean.QBTMud.Components
{ {
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{ {
var hasUpdates = false;
if (Active && Hash is not null) if (Active && Hash is not null)
{ {
IReadOnlyList<QBitTorrentClient.Models.FileData> files; IReadOnlyList<QBitTorrentClient.Models.FileData> files;
@@ -214,14 +222,20 @@ namespace Lantean.QBTMud.Components
if (FileList is null) if (FileList is null)
{ {
FileList = DataManager.CreateContentsList(files); FileList = DataManager.CreateContentsList(files);
hasUpdates = true;
} }
else 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); var contents = await ApiClient.GetTorrentContents(Hash);
FileList = DataManager.CreateContentsList(contents); FileList = DataManager.CreateContentsList(contents);
MarkFilesDirty();
PruneSelectionIfMissing();
var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}"); var expandedNodes = await LocalStorage.GetItemAsync<HashSet<string>>($"{_expandedNodesStorageKey}.{Hash}");
if (expandedNodes is not null) if (expandedNodes is not null)
@@ -257,6 +273,8 @@ namespace Lantean.QBTMud.Components
{ {
ExpandedNodes.Clear(); ExpandedNodes.Clear();
} }
MarkFilesDirty();
} }
protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority) protected async Task PriorityValueChanged(ContentItem contentItem, Priority priority)
@@ -321,11 +339,13 @@ namespace Lantean.QBTMud.Components
protected void SortColumnChanged(string sortColumn) protected void SortColumnChanged(string sortColumn)
{ {
_sortColumn = sortColumn; _sortColumn = sortColumn;
MarkFilesDirty();
} }
protected void SortDirectionChanged(SortDirection sortDirection) protected void SortDirectionChanged(SortDirection sortDirection)
{ {
_sortDirection = sortDirection; _sortDirection = sortDirection;
MarkFilesDirty();
} }
protected void SelectedItemChanged(ContentItem item) protected void SelectedItemChanged(ContentItem item)
@@ -344,6 +364,7 @@ namespace Lantean.QBTMud.Components
ExpandedNodes.Add(contentItem.Name); ExpandedNodes.Add(contentItem.Name);
} }
MarkFilesDirty();
await LocalStorage.SetItemAsync($"{_expandedNodesStorageKey}.{Hash}", ExpandedNodes); 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); 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) private bool FilterContentItem(ContentItem item)
{ {
if (Filters is not null) if (Filters is not null)
@@ -430,37 +413,130 @@ namespace Lantean.QBTMud.Components
} }
private ReadOnlyCollection<ContentItem> GetFiles() 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) if (FileList is null || FileList.Values.Count == 0)
{ {
return new ReadOnlyCollection<ContentItem>([]); return EmptyContentItems;
} }
var maxLevel = FileList.Values.Max(f => f.Level); var lookup = BuildChildrenLookup();
// this is a flat file structure if (!lookup.TryGetValue(string.Empty, out var roots))
if (maxLevel == 0)
{ {
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 orderedRoots)
foreach (var item in rootItems)
{ {
list.Add(item); if (item.IsFolder)
if (item.IsFolder && ExpandedNodes.Contains(item.Name))
{ {
var descendants = GetChildren(item); result.Add(item);
foreach (var descendant in descendants)
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() protected async Task DoNotDownloadLessThan100PercentAvailability()

View File

@@ -4,6 +4,7 @@ using Lantean.QBTMud.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using MudBlazor; using MudBlazor;
using System;
namespace Lantean.QBTMud.Components.UI namespace Lantean.QBTMud.Components.UI
{ {
@@ -81,6 +82,8 @@ namespace Lantean.QBTMud.Components.UI
protected HashSet<string> SelectedColumns { get; set; } = []; 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?> _columnWidths = [];
private Dictionary<string, int> _columnOrder = []; private Dictionary<string, int> _columnOrder = [];
@@ -91,6 +94,12 @@ namespace Lantean.QBTMud.Components.UI
private readonly Dictionary<string, TdExtended> _tds = []; 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() protected override async Task OnInitializedAsync()
{ {
HashSet<string> selectedColumns; HashSet<string> selectedColumns;
@@ -109,6 +118,13 @@ namespace Lantean.QBTMud.Components.UI
SelectedColumns = selectedColumns; SelectedColumns = selectedColumns;
await SelectedColumnsChanged.InvokeAsync(SelectedColumns); await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
} }
else
{
SelectedColumns = selectedColumns;
}
_lastColumnDefinitions = ColumnDefinitions;
MarkColumnsDirty();
string? sortColumn; string? sortColumn;
SortDirection sortDirection; SortDirection sortDirection;
@@ -137,11 +153,24 @@ namespace Lantean.QBTMud.Components.UI
await SortDirectionChanged.InvokeAsync(_sortDirection); await SortDirectionChanged.InvokeAsync(_sortDirection);
} }
MarkColumnsDirty();
var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey); var storedColumnsWidths = await LocalStorage.GetItemAsync<Dictionary<string, int?>>(_columnWidthsStorageKey);
if (storedColumnsWidths is not null) if (storedColumnsWidths is not null)
{ {
_columnWidths = storedColumnsWidths; _columnWidths = storedColumnsWidths;
} }
MarkColumnsDirty();
}
protected override void OnParametersSet()
{
base.OnParametersSet();
if (!ReferenceEquals(_lastColumnDefinitions, ColumnDefinitions))
{
_lastColumnDefinitions = ColumnDefinitions;
MarkColumnsDirty();
}
} }
private IEnumerable<T>? GetOrderedItems() private IEnumerable<T>? GetOrderedItems()
@@ -165,39 +194,74 @@ namespace Lantean.QBTMud.Components.UI
return Items.OrderByDirection(_sortDirection, sortSelector); 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 (!_columnsDirty)
if (_columnOrder.Count == 0)
{ {
foreach (var column in filteredColumns) return _visibleColumns;
{
if (_columnWidths.TryGetValue(column.Id, out var value))
{
column.Width = value;
}
yield return column;
}
yield break;
} }
var columnDictionary = filteredColumns.ToDictionary(c => c.Id); _visibleColumns = BuildVisibleColumns();
foreach (var columnId in _columnOrder.OrderBy(c => c.Value).Select(c => c.Key)) _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)) if (_columnWidths.TryGetValue(column.Id, out var value))
{ {
column.Width = value; column.Width = value;
} }
yield return column;
} }
return orderedColumns;
} }
private async Task SetSort(string columnId, SortDirection sortDirection) private async Task SetSort(string columnId, SortDirection sortDirection)
@@ -316,18 +380,21 @@ namespace Lantean.QBTMud.Components.UI
SelectedColumns = result.SelectedColumns; SelectedColumns = result.SelectedColumns;
await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns); await LocalStorage.SetItemAsync(_columnSelectionStorageKey, SelectedColumns);
await SelectedColumnsChanged.InvokeAsync(SelectedColumns); await SelectedColumnsChanged.InvokeAsync(SelectedColumns);
MarkColumnsDirty();
} }
if (!DictionaryEqual(_columnWidths, result.ColumnWidths)) if (!DictionaryEqual(_columnWidths, result.ColumnWidths))
{ {
_columnWidths = result.ColumnWidths; _columnWidths = result.ColumnWidths;
await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths); await LocalStorage.SetItemAsync(_columnWidthsStorageKey, _columnWidths);
MarkColumnsDirty();
} }
if (!DictionaryEqual(_columnOrder, result.ColumnOrder)) if (!DictionaryEqual(_columnOrder, result.ColumnOrder))
{ {
_columnOrder = result.ColumnOrder; _columnOrder = result.ColumnOrder;
await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder); await LocalStorage.SetItemAsync(_columnOrderStorageKey, _columnOrder);
MarkColumnsDirty();
} }
} }
@@ -379,6 +446,12 @@ namespace Lantean.QBTMud.Components.UI
return className; return className;
} }
private void MarkColumnsDirty()
{
_columnsDirty = true;
_visibleColumns = EmptyColumns;
}
private sealed record SortData private sealed record SortData
{ {
public SortData(string sortColumn, SortDirection sortDirection) public SortData(string sortColumn, SortDirection sortDirection)
@@ -392,4 +465,4 @@ namespace Lantean.QBTMud.Components.UI
public SortDirection SortDirection { get; init; } public SortDirection SortDirection { get; init; }
} }
} }
} }

View File

@@ -745,6 +745,111 @@ namespace Lantean.QBTMud.Services
return result; 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 private sealed class ContentTreeNode
{ {
public ContentTreeNode(ContentItem? item, ContentTreeNode? parent) public ContentTreeNode(ContentItem? item, ContentTreeNode? parent)
@@ -1180,24 +1285,120 @@ namespace Lantean.QBTMud.Services
return original; 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); if (files.Count == 0)
foreach (var (key, value) in contentsList)
{ {
if (contents.TryGetValue(key, out var content)) if (contents.Count == 0)
{ {
content.Availability = value.Availability; return false;
content.Priority = value.Priority; }
content.Progress = value.Progress;
content.Size = value.Size; 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 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) public RssList CreateRssList(IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> rssItems)

View File

@@ -16,7 +16,7 @@ namespace Lantean.QBTMud.Services
Dictionary<string, ContentItem> CreateContentsList(IReadOnlyList<QBitTorrentClient.Models.FileData> files); 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); QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed);