From dd7b10cf68196550d732c90e51a8cc1fd987e54b Mon Sep 17 00:00:00 2001 From: ahjephson Date: Fri, 3 May 2024 14:59:52 +0100 Subject: [PATCH] Update torrent actions --- .../Dialogs/AddTorrentLinkDialog.razor | 2 +- .../Dialogs/AddTorrentOptions.razor | 30 +- .../Dialogs/RenameFilesDialog.razor | 12 + .../Dialogs/RenameFilesDialog.razor.cs | 23 ++ .../Components/Dialogs/RssRulesDialog.razor | 12 + .../Dialogs/RssRulesDialog.razor.cs | 22 ++ .../Components/Dialogs/StatisticsDialog.razor | 5 - .../Dialogs/StatisticsDialog.razor.cs | 11 - Lantean.QBTMudBlade/Components/FilesTab.razor | 4 +- .../Components/FilesTab.razor.cs | 42 ++- .../Components/GeneralTab.razor | 4 +- .../Components/GeneralTab.razor.cs | 10 +- .../Components/Options/AdvancedOptions.razor | 2 +- .../Options/AdvancedOptions.razor.cs | 4 +- .../Options/ConnectionOptions.razor | 2 +- .../Components/Options/DownloadsOptions.razor | 10 +- .../Components/Options/RSSOptions.razor | 8 +- .../Components/Options/RSSOptions.razor.cs | 10 +- .../Components/Options/WebUIOptions.razor.cs | 24 +- .../Components/PeersTab.razor.cs | 2 +- .../Components/PieceProgress.razor | 1 + .../Components/PieceProgress.razor.cs | 77 +++++ .../Components/TorrentActions.razor | 102 ++++++- .../Components/TorrentActions.razor.cs | 44 ++- .../Components/TorrentInfo.razor | 23 ++ .../Components/TorrentInfo.razor.cs | 27 ++ .../Components/TrackersTab.razor.cs | 3 +- .../Components/WebSeedsTab.razor.cs | 3 +- Lantean.QBTMudBlade/DialogHelper.cs | 5 + Lantean.QBTMudBlade/Interop/InteropHelper.cs | 15 + .../Lantean.QBTMudBlade.csproj | 6 +- .../Layout/LoggedInLayout.razor | 16 +- .../Layout/LoggedInLayout.razor.cs | 3 + Lantean.QBTMudBlade/Pages/Details.razor | 4 +- Lantean.QBTMudBlade/Pages/Login.razor.cs | 2 +- .../Pages/TorrentList.razor.cs | 27 ++ Lantean.QBTMudBlade/wwwroot/css/app.css | 28 +- Lantean.QBTMudBlade/wwwroot/index.html | 2 + Lantean.QBTMudBlade/wwwroot/js/interop.js | 35 ++- Lantean.QBTMudBlade/wwwroot/js/piecesbar.js | 273 ++++++++++++++++++ 40 files changed, 842 insertions(+), 93 deletions(-) create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor create mode 100644 Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor.cs delete mode 100644 Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor delete mode 100644 Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/PieceProgress.razor create mode 100644 Lantean.QBTMudBlade/Components/PieceProgress.razor.cs create mode 100644 Lantean.QBTMudBlade/Components/TorrentInfo.razor create mode 100644 Lantean.QBTMudBlade/Components/TorrentInfo.razor.cs create mode 100644 Lantean.QBTMudBlade/wwwroot/js/piecesbar.js diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor index b6ac2d1..46ab783 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentLinkDialog.razor @@ -2,7 +2,7 @@ - + diff --git a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor index e912139..c7770e3 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor +++ b/Lantean.QBTMudBlade/Components/Dialogs/AddTorrentOptions.razor @@ -1,23 +1,23 @@  - + Manual Automatic - + @if (ShowCookieOption) { - + } - + - + @foreach (var category in Categories) { @category @@ -25,38 +25,38 @@ - + - + - + None Metadata received Files checked - + - + - + Original Create subfolder Don't create subfolder' - + - + - + - + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor new file mode 100644 index 0000000..3766142 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor @@ -0,0 +1,12 @@ + + + + + + + + + Close + Rename + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor.cs new file mode 100644 index 0000000..987c523 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/RenameFilesDialog.razor.cs @@ -0,0 +1,23 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class RenameFilesDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor new file mode 100644 index 0000000..104a80c --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor @@ -0,0 +1,12 @@ + + + + + + + + + Close + Save + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor.cs new file mode 100644 index 0000000..97c3c0c --- /dev/null +++ b/Lantean.QBTMudBlade/Components/Dialogs/RssRulesDialog.razor.cs @@ -0,0 +1,22 @@ +using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Web; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Dialogs +{ + public partial class RssRulesDialog + { + [CascadingParameter] + public MudDialogInstance MudDialog { get; set; } = default!; + + protected void Cancel(MouseEventArgs args) + { + MudDialog.Cancel(); + } + + protected void Submit(MouseEventArgs args) + { + MudDialog.Close(); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor deleted file mode 100644 index 2351153..0000000 --- a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor +++ /dev/null @@ -1,5 +0,0 @@ - - - Statistics - - \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs deleted file mode 100644 index 0d1f363..0000000 --- a/Lantean.QBTMudBlade/Components/Dialogs/StatisticsDialog.razor.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Microsoft.AspNetCore.Components; -using MudBlazor; - -namespace Lantean.QBTMudBlade.Components.Dialogs -{ - public partial class StatisticsDialog - { - [CascadingParameter] - public MudDialogInstance MudDialog { get; set; } = default!; - } -} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor b/Lantean.QBTMudBlade/Components/FilesTab.razor index c00dc56..ba336e3 100644 --- a/Lantean.QBTMudBlade/Components/FilesTab.razor +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor @@ -13,12 +13,12 @@ - + Less Than 100% Availability Less than 80% Availability Currently Filtered Files - + Less Than 100% Availability Less than 80% Availability Currently Filtered Files diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs index 005bba3..1a12021 100644 --- a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs @@ -1,4 +1,5 @@ -using Lantean.QBitTorrentClient; +using Blazored.LocalStorage; +using Lantean.QBitTorrentClient; using Lantean.QBTMudBlade.Components.Dialogs; using Lantean.QBTMudBlade.Filter; using Lantean.QBTMudBlade.Models; @@ -8,12 +9,15 @@ using Microsoft.AspNetCore.Components.Web; using MudBlazor; using System.Collections.ObjectModel; using System.Net; -using static MudBlazor.CategoryTypes; namespace Lantean.QBTMudBlade.Components { public partial class FilesTab : IAsyncDisposable { + private readonly bool _refreshEnabled = true; + + private const string _columnStorageKey = "FilesTab.Columns"; + private readonly CancellationTokenSource _timerCancellationToken = new(); private bool _disposedValue; @@ -39,6 +43,9 @@ namespace Lantean.QBTMudBlade.Components [Inject] protected IDataManager DataManager { get; set; } = default!; + [Inject] + protected ILocalStorageService LocalStorage { get; set; } = default!; + protected HashSet ExpandedNodes { get; set; } = []; protected Dictionary? FileList { get; set; } @@ -71,6 +78,22 @@ namespace Lantean.QBTMudBlade.Components SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet(); } + protected override async Task OnInitializedAsync() + { + if (!await LocalStorage.ContainKeyAsync(_columnStorageKey)) + { + return; + } + + var selectedColumns = await LocalStorage.GetItemAsync>(_columnStorageKey); + if (selectedColumns is null) + { + return; + } + + SelectedColumns = selectedColumns; + } + protected IEnumerable> GetColumns() { return _columns.Where(c => SelectedColumns.Contains(c.Id)); @@ -92,6 +115,8 @@ namespace Lantean.QBTMudBlade.Components } SelectedColumns = (HashSet)result.Data; + + await LocalStorage.SetItemAsync(_columnStorageKey, SelectedColumns); } protected async Task ShowFilterDialog() @@ -163,7 +188,7 @@ namespace Lantean.QBTMudBlade.Components _timerCancellationToken.Cancel(); _timerCancellationToken.Dispose(); - await Task.Delay(0); + await Task.CompletedTask; } _disposedValue = true; @@ -193,6 +218,11 @@ namespace Lantean.QBTMudBlade.Components protected override async Task OnAfterRenderAsync(bool firstRender) { + if (!_refreshEnabled) + { + return; + } + if (!firstRender) { return; @@ -304,11 +334,11 @@ namespace Lantean.QBTMudBlade.Components protected string RowStyle(ContentItem item, int index) { var style = "user-select: none; cursor: pointer;"; - if (_selectedIndex == item.Index) + if (_selectedIndex != item.Index) { - style += " background: #D3D3D3"; + return style; } - return style; + return $"{style} background: #D3D3D3"; } protected async Task SelectedItemsChanged(HashSet selectedItems) diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor b/Lantean.QBTMudBlade/Components/GeneralTab.razor index af41d54..50079d4 100644 --- a/Lantean.QBTMudBlade/Components/GeneralTab.razor +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor @@ -1,4 +1,6 @@  + Progress + Transfer @@ -53,7 +55,7 @@ - Information + Information @DisplayHelpers.Size(Properties?.TotalSize) diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs index 4e409f5..ad867e5 100644 --- a/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs @@ -8,6 +8,8 @@ namespace Lantean.QBTMudBlade.Components { public partial class GeneralTab : IAsyncDisposable { + private readonly bool _refreshEnabled = true; + private readonly CancellationTokenSource _timerCancellationToken = new(); private bool _disposedValue; @@ -50,6 +52,11 @@ namespace Lantean.QBTMudBlade.Components protected override async Task OnAfterRenderAsync(bool firstRender) { + if (!_refreshEnabled) + { + return; + } + if (firstRender) { using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(RefreshInterval))) @@ -84,7 +91,8 @@ namespace Lantean.QBTMudBlade.Components { _timerCancellationToken.Cancel(); _timerCancellationToken.Dispose(); - await Task.Delay(0); + + await Task.CompletedTask; } _disposedValue = true; diff --git a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor index 1f6606a..0bd25e3 100644 --- a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor +++ b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor @@ -15,7 +15,7 @@ - MiB + diff --git a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs index 4fa44f5..ae250f9 100644 --- a/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs +++ b/Lantean.QBTMudBlade/Components/Options/AdvancedOptions.razor.cs @@ -607,7 +607,5 @@ namespace Lantean.QBTMudBlade.Components.Options UpdatePreferences.I2pOutboundLength = value; await PreferencesChanged.InvokeAsync(UpdatePreferences); } - - } -} +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor index 0f20bd4..ad37881 100644 --- a/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor +++ b/Lantean.QBTMudBlade/Components/Options/ConnectionOptions.razor @@ -81,7 +81,7 @@ - + diff --git a/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor index ff830f8..55ae625 100644 --- a/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor +++ b/Lantean.QBTMudBlade/Components/Options/DownloadsOptions.razor @@ -86,7 +86,7 @@ - + @@ -94,7 +94,7 @@ - + @@ -104,7 +104,7 @@ - + @@ -114,7 +114,7 @@ - + @@ -222,7 +222,7 @@ - + diff --git a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor index 6b894fc..beb143f 100644 --- a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor +++ b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor @@ -9,10 +9,10 @@ - + - min + @@ -30,7 +30,7 @@ - + Edit auto downloading rules @@ -48,7 +48,7 @@ - + diff --git a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs index f4076f8..d930934 100644 --- a/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs +++ b/Lantean.QBTMudBlade/Components/Options/RSSOptions.razor.cs @@ -1,7 +1,13 @@ -namespace Lantean.QBTMudBlade.Components.Options +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components.Options { public partial class RSSOptions : Options { + [Inject] + public IDialogService DialogService { get; set; } = default!; + protected bool RssProcessingEnabled { get; private set; } protected int RssRefreshInterval { get; private set; } protected long RssFetchDelay { get; private set; } @@ -79,7 +85,7 @@ protected async Task OpenRssRulesDialog() { - await Task.Delay(0); + await DialogService.InvokeRssRulesDialog(); } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs index a722d04..daf571a 100644 --- a/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs +++ b/Lantean.QBTMudBlade/Components/Options/WebUIOptions.razor.cs @@ -1,7 +1,14 @@ -namespace Lantean.QBTMudBlade.Components.Options +using Lantean.QBTMudBlade.Interop; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; + +namespace Lantean.QBTMudBlade.Components.Options { public partial class WebUIOptions : Options { + [Inject] + public IJSRuntime JSRuntime { get; set; } = default!; + protected string? Locale { get; private set; } protected bool PerformanceWarning { get; private set; } protected string? WebUiDomainList { get; private set; } @@ -385,7 +392,20 @@ protected async Task RegisterDyndnsService() { - await Task.Delay(0); + if (!DyndnsEnabled) + { + return; + } + +#pragma warning disable S1075 // URIs should not be hardcoded + var url = DyndnsService switch + { + 0 => "https://www.dyndns.com/account/services/hosts/add.html", + 1 => "http://www.no-ip.com/services/managed_dns/free_dynamic_dns.html", + _ => throw new InvalidOperationException($"DyndnsService value of {DyndnsService} is not supported."), + }; +#pragma warning restore S1075 // URIs should not be hardcoded + await JSRuntime.Open(url, true); } } } diff --git a/Lantean.QBTMudBlade/Components/PeersTab.razor.cs b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs index fbd6b12..1231ea4 100644 --- a/Lantean.QBTMudBlade/Components/PeersTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs @@ -102,7 +102,7 @@ namespace Lantean.QBTMudBlade.Components _timerCancellationToken.Cancel(); _timerCancellationToken.Dispose(); - await Task.Delay(0); + await Task.CompletedTask; } _disposedValue = true; diff --git a/Lantean.QBTMudBlade/Components/PieceProgress.razor b/Lantean.QBTMudBlade/Components/PieceProgress.razor new file mode 100644 index 0000000..54abb64 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/PieceProgress.razor @@ -0,0 +1 @@ +
\ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/PieceProgress.razor.cs b/Lantean.QBTMudBlade/Components/PieceProgress.razor.cs new file mode 100644 index 0000000..67cc98b --- /dev/null +++ b/Lantean.QBTMudBlade/Components/PieceProgress.razor.cs @@ -0,0 +1,77 @@ +using Lantean.QBitTorrentClient.Models; +using Lantean.QBTMudBlade.Interop; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using MudBlazor.Services; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class PieceProgress : IBrowserViewportObserver, IAsyncDisposable + { + private bool _disposedValue; + + [Inject] + public IJSRuntime JSRuntime { get; set; } = default!; + + [Inject] + private IBrowserViewportService BrowserViewportService { get; set; } = default!; + + [Parameter] + [EditorRequired] + public string Hash { get; set; } = default!; + + [Parameter] + [EditorRequired] + public IReadOnlyList Pieces { get; set; } = []; + + public Guid Id => Guid.NewGuid(); + + protected override async Task OnParametersSetAsync() + { + await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray()); + } + + ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new() + { + ReportRate = 50, + NotifyOnBreakpointOnly = false + }; + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + await BrowserViewportService.SubscribeAsync(this, fireImmediately: true); + } + + await base.OnAfterRenderAsync(firstRender); + } + + public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs) + { + await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray()); + await InvokeAsync(StateHasChanged); + } + + protected virtual async Task DisposeAsync(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + await BrowserViewportService.UnsubscribeAsync(this); + } + + _disposedValue = true; + } + } + + public async ValueTask DisposeAsync() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + await DisposeAsync(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor b/Lantean.QBTMudBlade/Components/TorrentActions.razor index 5a13dec..3508a38 100644 --- a/Lantean.QBTMudBlade/Components/TorrentActions.razor +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor @@ -1,13 +1,71 @@ -@if (Type == ParentType.StandaloneToolbar) +@if (Type == ParentType.Toolbar) { - + @ToolbarContent } -else if (Type == ParentType.Toolbar) +else if (Type == ParentType.ToolbarContents) { @ToolbarContent } +else if (Type == ParentType.MixedToolbar) +{ + + @MixedToolbarContent + +} +else if (Type == ParentType.MixedToolbarContents) +{ + @MixedToolbarContent +} +else if (Type == ParentType.InitialIconsOnly) +{ + @foreach (var option in GetOptions().Take(5)) + { + @if (option is Divider) + { + + } + else + { + + } + } + + + @foreach (var option in GetOptions().Skip(5)) + { + @if (option is Divider) + { + + } + else if (!option.Children.Any()) + { + + @option.Name + + } + else + { + + + + @option.Name + + + + @foreach (var childItem in option.Children) + { + @ChildItem(childItem) + } + + + + } + + } + +} else { @@ -24,9 +82,9 @@ else } else { - + - @option.Name + @option.Name @@ -64,7 +122,39 @@ else } else { - + + @foreach (var childItem in option.Children) + { + @ChildItem(childItem) + } + + } + } + + ; + + private RenderFragment MixedToolbarContent => + @ + @foreach (var option in GetOptions()) + { + @if (option is Divider) + { + + } + else if (!option.Children.Any()) + { + if (option.Icon is null) + { + @option.Name + } + else + { + + } + } + else + { + @foreach (var childItem in option.Children) { @ChildItem(childItem) diff --git a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs index cf1dea0..b94a6d3 100644 --- a/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs +++ b/Lantean.QBTMudBlade/Components/TorrentActions.razor.cs @@ -44,6 +44,11 @@ namespace Lantean.QBTMudBlade.Components [CascadingParameter] public MainData MainData { get; set; } = default!; + [CascadingParameter] + public QBitTorrentClient.Models.Preferences? Preferences { get; set; } + + protected MudMenu? ActionsMenu { get; set; } + protected async Task Pause() { await ApiClient.PauseTorrents(Hashes); @@ -203,6 +208,7 @@ namespace Lantean.QBTMudBlade.Components protected async Task Copy(Func selector) { await Copy(string.Join(Environment.NewLine, GetTorrents().Select(selector))); + ActionsMenu?.CloseMenu(); } protected async Task Export() @@ -260,8 +266,8 @@ namespace Lantean.QBTMudBlade.Components new Divider(), new Action("Set location", Icons.Material.Filled.MyLocation, Color.Info, EventCallback.Factory.Create(this, SetLocation)), new Action("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, EventCallback.Factory.Create(this, Rename)), - new Action("Category", Icons.Material.Filled.List, Color.Info, categories), - new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags), + new Action("Category", Icons.Material.Filled.List, Color.Info, categories, true), + new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags, true), new Action("Automatic Torrent Management", Icons.Material.Filled.Check, firstTorrent.AutomaticTorrentManagement ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)), new Divider(), new Action("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, EventCallback.Factory.Create(this, LimitUploadRate)), @@ -289,15 +295,38 @@ namespace Lantean.QBTMudBlade.Components new Action("Export", Icons.Material.Filled.SaveAlt, Color.Info, EventCallback.Factory.Create(this, Export)), }; + if (Preferences?.QueueingEnabled == false) + { + options.RemoveAt(18); + } + return options; } } public enum ParentType { + /// + /// Renders toolbar contents without the wrapper. + /// + ToolbarContents, + /// + /// Renders a . + /// Toolbar, - StandaloneToolbar, + /// + /// Renders a . + /// Menu, + /// + /// Renders a with for basic actions and a for actions with children. + /// + MixedToolbarContents, + /// + /// Renders toolbar contents without the wrapper with for basic actions and a for actions with children. + /// + MixedToolbar, + InitialIconsOnly, } public class Divider : Action @@ -309,7 +338,7 @@ namespace Lantean.QBTMudBlade.Components public class Action { - public Action(string name, string icon, Color color, EventCallback callback) + public Action(string name, string? icon, Color color, EventCallback callback) { Name = name; Icon = icon; @@ -318,23 +347,26 @@ namespace Lantean.QBTMudBlade.Components Children = []; } - public Action(string name, string icon, Color color, IEnumerable children) + public Action(string name, string? icon, Color color, IEnumerable children, bool useTextButton = false) { Name = name; Icon = icon; Color = color; Callback = default; Children = children; + UseTextButton = useTextButton; } public string Name { get; } - public string Icon { get; } + public string? Icon { get; } public Color Color { get; } public EventCallback Callback { get; } public IEnumerable Children { get; } + + public bool UseTextButton { get; } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentInfo.razor b/Lantean.QBTMudBlade/Components/TorrentInfo.razor new file mode 100644 index 0000000..255c34c --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentInfo.razor @@ -0,0 +1,23 @@ +@if (Torrent is null) +{ + return; +} + + + @{ + var (icon, color) = DisplayHelpers.GetStateIcon(Torrent.State); + } + + @Torrent.Name + + @DisplayHelpers.Size(Torrent.Size) + + @{ + var value = Torrent.Progress; + var progressColor = value < 1 ? Color.Success : Color.Info; + + @DisplayHelpers.Percentage(value) + + ; + } + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/TorrentInfo.razor.cs b/Lantean.QBTMudBlade/Components/TorrentInfo.razor.cs new file mode 100644 index 0000000..6147d91 --- /dev/null +++ b/Lantean.QBTMudBlade/Components/TorrentInfo.razor.cs @@ -0,0 +1,27 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; + +namespace Lantean.QBTMudBlade.Components +{ + public partial class TorrentInfo + { + [Parameter] + [EditorRequired] + public string Hash { get; set; } = default!; + + [CascadingParameter] + public MainData MainData { get; set; } = default!; + + protected Torrent? Torrent => GetTorrent(); + + private Torrent? GetTorrent() + { + if (Hash is null || !MainData.Torrents.TryGetValue(Hash, out var torrent)) + { + return null; + } + + return torrent; + } + } +} diff --git a/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs index e98f51c..8379c0a 100644 --- a/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/TrackersTab.razor.cs @@ -80,7 +80,8 @@ namespace Lantean.QBTMudBlade.Components { _timerCancellationToken.Cancel(); _timerCancellationToken.Dispose(); - await Task.Delay(0); + + await Task.CompletedTask; } _disposedValue = true; diff --git a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs index c505102..3184dc5 100644 --- a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor.cs @@ -43,7 +43,8 @@ namespace Lantean.QBTMudBlade.Components { _timerCancellationToken.Cancel(); _timerCancellationToken.Dispose(); - await Task.Delay(0); + + await Task.CompletedTask; } _disposedValue = true; diff --git a/Lantean.QBTMudBlade/DialogHelper.cs b/Lantean.QBTMudBlade/DialogHelper.cs index 6fa4393..5f0e6c7 100644 --- a/Lantean.QBTMudBlade/DialogHelper.cs +++ b/Lantean.QBTMudBlade/DialogHelper.cs @@ -225,5 +225,10 @@ namespace Lantean.QBTMudBlade return (List>?)dialogResult.Data; } + + public static async Task InvokeRssRulesDialog(this IDialogService dialogService) + { + await Task.Delay(0); + } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Interop/InteropHelper.cs b/Lantean.QBTMudBlade/Interop/InteropHelper.cs index 307c748..473d0cd 100644 --- a/Lantean.QBTMudBlade/Interop/InteropHelper.cs +++ b/Lantean.QBTMudBlade/Interop/InteropHelper.cs @@ -13,5 +13,20 @@ namespace Lantean.QBTMudBlade.Interop { await runtime.InvokeVoidAsync("qbt.triggerFileDownload", url, filename); } + + public static async Task Open(this IJSRuntime runtime, string url, bool newTab = false) + { + string? target = null; + if (newTab) + { + target = url; + } + await runtime.InvokeVoidAsync("qbt.open", url, target); + } + + public static async Task RenderPiecesBar(this IJSRuntime runtime, string id, string hash, int[] pieces) + { + await runtime.InvokeVoidAsync("qbt.renderPiecesBar", id, hash, pieces); + } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj index 31be248..588c799 100644 --- a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj +++ b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj @@ -10,10 +10,10 @@ - - + + - + diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor index e60ee77..a368d9e 100644 --- a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor @@ -11,13 +11,15 @@ - - - - - - - @Body + + + + + + + + @Body + diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs index a51f76f..3d55d76 100644 --- a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor.cs @@ -37,6 +37,8 @@ namespace Lantean.QBTMudBlade.Layout protected Status Status { get; set; } = Status.All; + protected QBitTorrentClient.Models.Preferences? Preferences { get; set; } + protected string Version { get; set; } = ""; protected string? SearchText { get; set; } @@ -69,6 +71,7 @@ namespace Lantean.QBTMudBlade.Layout await InvokeAsync(StateHasChanged); + Preferences = await ApiClient.GetApplicationPreferences(); Version = await ApiClient.GetApplicationVersion(); var data = await ApiClient.GetMainData(_requestId); MainData = DataManager.CreateMainData(data); diff --git a/Lantean.QBTMudBlade/Pages/Details.razor b/Lantean.QBTMudBlade/Pages/Details.razor index 2a516c0..eb4c938 100644 --- a/Lantean.QBTMudBlade/Pages/Details.razor +++ b/Lantean.QBTMudBlade/Pages/Details.razor @@ -9,10 +9,10 @@ } @if (Hash is not null) { - + } - @Name + @Name diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs index 56d4bf1..351c6c5 100644 --- a/Lantean.QBTMudBlade/Pages/Login.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages #if DEBUG protected override async Task OnInitializedAsync() { - await DoLogin("admin", "A9tExgXcZ"); + await DoLogin("admin", "p22bMTJDK"); } #endif } diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs index 3e50774..2a4e313 100644 --- a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs @@ -10,6 +10,8 @@ namespace Lantean.QBTMudBlade.Pages { public partial class TorrentList { + private const string _columnStorageKey = "TorrentList.Columns"; + [Inject] protected IApiClient ApiClient { get; set; } = default!; @@ -22,6 +24,9 @@ namespace Lantean.QBTMudBlade.Pages [Inject] protected NavigationManager NavigationManager { get; set; } = default!; + [CascadingParameter] + public QBitTorrentClient.Models.Preferences? Preferences { get; set; } + [CascadingParameter] public IEnumerable? Torrents { get; set; } @@ -38,11 +43,31 @@ namespace Lantean.QBTMudBlade.Pages protected bool ToolbarButtonsEnabled => SelectedItems.Count > 0 || SelectedTorrent is not null; + protected override async Task OnInitializedAsync() + { + if (!await LocalStorage.ContainKeyAsync(_columnStorageKey)) + { + return; + } + + var selectedColumns = await LocalStorage.GetItemAsync>(_columnStorageKey); + if (selectedColumns is null) + { + return; + } + + SelectedColumns = selectedColumns; + } + protected override void OnParametersSet() { if (SelectedColumns.Count == 0) { SelectedColumns = _columns.Where(c => c.Enabled).Select(c => c.Id).ToHashSet(); + if (Preferences?.QueueingEnabled == false) + { + SelectedColumns.Remove("#"); + } } _sortSelector ??= _columns.First(c => c.Enabled).SortSelector; } @@ -183,6 +208,8 @@ namespace Lantean.QBTMudBlade.Pages } SelectedColumns = (HashSet)result.Data; + + await LocalStorage.SetItemAsync(_columnStorageKey, SelectedColumns); } protected void ShowTorrent() diff --git a/Lantean.QBTMudBlade/wwwroot/css/app.css b/Lantean.QBTMudBlade/wwwroot/css/app.css index c4ad4ff..a218aa0 100644 --- a/Lantean.QBTMudBlade/wwwroot/css/app.css +++ b/Lantean.QBTMudBlade/wwwroot/css/app.css @@ -122,4 +122,30 @@ td.no-wrap { .field-switch { -} \ No newline at end of file +} + +.piecesbarWrapper { + position: relative; +} + +.piecesbarCanvas { + height: 100%; + image-rendering: pixelated; + inset: 0; + position: absolute; + width: 100%; +} + +.sub-menu { + width: 100%; +} + + .sub-menu::after { + content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M0 0h24v24H0V0z' fill='none'/%3E%3Cpath d='M7.41 8.59L12 13.17l4.59-4.58L18 10l-6 6-6-6 1.41-1.41z'/%3E%3C/svg%3E"); + width: 25px; + height: 25px; + position: absolute; + top: -2px; + right: -5px; + transform: rotate(270deg); + } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/wwwroot/index.html b/Lantean.QBTMudBlade/wwwroot/index.html index 0ecafe6..7bd6672 100644 --- a/Lantean.QBTMudBlade/wwwroot/index.html +++ b/Lantean.QBTMudBlade/wwwroot/index.html @@ -30,6 +30,8 @@ + + diff --git a/Lantean.QBTMudBlade/wwwroot/js/interop.js b/Lantean.QBTMudBlade/wwwroot/js/interop.js index 6b390b1..690c4e4 100644 --- a/Lantean.QBTMudBlade/wwwroot/js/interop.js +++ b/Lantean.QBTMudBlade/wwwroot/js/interop.js @@ -1,6 +1,8 @@ -const qbt = {}; +if (window.qbt === undefined) { + window.qbt = {}; +} -qbt.triggerFileDownload = (url, fileName) => { +window.qbt.triggerFileDownload = (url, fileName) => { const anchorElement = document.createElement('a'); anchorElement.href = url; anchorElement.download = fileName ?? ''; @@ -8,10 +10,35 @@ qbt.triggerFileDownload = (url, fileName) => { anchorElement.remove(); } -qbt.getBoundingClientRect = (id) => { +window.qbt.getBoundingClientRect = (id) => { const element = document.getElementById(id); return element.getBoundingClientRect(); } -window.qbt = qbt; \ No newline at end of file +window.qbt.open = (url, target) => { + window.open(url, target); +} + +window.qbt.renderPiecesBar = (id, hash, pieces) => { + const parentElement = document.getElementById(id); + if (window.qbt.hash !== hash) { + if (parentElement) { + while (parentElement.lastElementChild) { + parentElement.removeChild(parentElement.lastElementChild); + } + } + window.qbt.hash = hash; + window.qbt.piecesBar = new window.qbt.PiecesBar([], { + height: 24 + }); + window.qbt.piecesBar.clear(); + } + + if (parentElement && !parentElement.hasChildNodes()) { + const el = window.qbt.piecesBar.createElement(); + parentElement.appendChild(el); + } + + window.qbt.piecesBar.setPieces(pieces); +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/wwwroot/js/piecesbar.js b/Lantean.QBTMudBlade/wwwroot/js/piecesbar.js new file mode 100644 index 0000000..515c0c2 --- /dev/null +++ b/Lantean.QBTMudBlade/wwwroot/js/piecesbar.js @@ -0,0 +1,273 @@ +/* + * Bittorrent Client using Qt and libtorrent. + * Copyright (C) 2022 Jesse Smick + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + * + * In addition, as a special exception, the copyright holders give permission to + * link this program with the OpenSSL project's "OpenSSL" library (or with + * modified versions of it that use the same license as the "OpenSSL" library), + * and distribute the linked executables. You must obey the GNU General Public + * License in all respects for all of the code used other than "OpenSSL". If you + * modify file(s), you may extend this exception to your version of the file(s), + * but you are not obligated to do so. If you do not wish to do so, delete this + * exception statement from your version. + */ + +'use strict'; + +if (window.qbt === undefined) { + window.qbt = {}; +} + +window.qbt.piecesBarUniqueId = 0; + +class PiecesBar { + STATUS_DOWNLOADING = 1; + STATUS_DOWNLOADED = 2; + + // absolute max width of 4096 + // this is to support all browsers for size of canvas elements + // see https://github.com/jhildenbiddle/canvas-size#test-results + MAX_CANVAS_WIDTH = 4096; + + constructor(pieces, parameters) { + + this.id = 'piecesbar_' + (window.qbt.piecesBarUniqueId++); + this.width = 0; + this.height = 0; + this.downloadingColor = 'green'; + this.haveColor = 'blue'; + this.borderSize = 1; + this.borderColor = '#999'; + + if (parameters && (typeof(parameters) === 'object')) { + Object.assign(this, parameters) + } + + this.height = Math.max(this.height, 12); + this.setPieces(pieces); + } + + createElement() { + this.obj = document.createElement('div'); + this.obj.className = 'piecesbarWrapper'; + this.obj.id = this.id; + this.obj.style = 'border: ' + this.borderSize.toString() + 'px solid ' + this.borderColor + '; height: ' + this.height.toString() + 'px'; + + this.canvas = document.createElement('canvas'); + this.canvas.id = this.id + '_canvas'; + this.canvas.className = 'piecesbarCanvas'; + this.canvas.width = (this.width - (2 * this.borderSize)).toString(); + + this.obj.appendChild(this.canvas); + + if (this.width > 0) { + this.setPieces(this.pieces); + } else { + setTimeout(() => { this.checkForParent(this.id); }, 1); + } + + return this.obj; + } + + clear() { + this.setPieces([]); + } + + setPieces(pieces) { + if (!Array.isArray(pieces)) { + this.pieces = []; + } else { + this.pieces = pieces; + } + this.refresh(true); + } + + refresh(force) { + if (!this.obj?.parentNode) { + return; + } + + const pieces = this.pieces; + + // if the number of pieces is small, use that for the width, + // and have it stretch horizontally. + // this also limits the ratio below to >= 1 + const width = Math.min(this.obj.offsetWidth, this.pieces.length, this.MAX_CANVAS_WIDTH); + if ((this.width === width) && !force) { + return; + } + + this.width = width; + + // change canvas size to fit exactly in the space + this.canvas.width = width - (2 * this.borderSize); + + const canvas = this.canvas; + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + + const imageWidth = canvas.width; + + if (imageWidth.length === 0) + return; + + let minStatus = Infinity; + let maxStatus = 0; + + for (const status of pieces) { + if (status > maxStatus) { + maxStatus = status; + } + + if (status < minStatus) { + minStatus = status; + } + } + + // if no progress then don't do anything + if (maxStatus === 0) { + return; + } + + // if all pieces are downloaded, fill entire image at once + if (minStatus === this.STATUS_DOWNLOADED) { + ctx.fillStyle = this.haveColor; + ctx.fillRect(0, 0, canvas.width, canvas.height); + return; + } + + /* Linear transformation from pieces to pixels. + * + * The canvas size can vary in width so this figures out what to draw at each pixel. + * Inspired by the GUI code here https://github.com/qbittorrent/qBittorrent/blob/25b3f2d1a6b14f0fe098fb79a3d034607e52deae/src/gui/properties/downloadedpiecesbar.cpp#L54 + * + * example ratio > 1 (at least 2 pieces per pixel) + * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ + * pieces | 2 | 1 | 2 | 0 | 2 | 0 | 1 | 0 | 1 | 2 | + * +---------+---------+---------+---------+---------+---------+ + * pixels | | | | | | | + * +---------+---------+---------+---------+---------+---------+ + * + * example ratio < 1 (at most 2 pieces per pixel) + * This case shouldn't happen since the max pixels are limited to the number of pieces + * +---------+---------+---------+---------+----------+--------+ + * pieces | 2 | 1 | 1 | 0 | 2 | 2 | + * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ + * pixels | | | | | | | | | | | + * +-----+-----+-----+-----+-----+-----+-----+-----+-----+-----+ + */ + + const ratio = pieces.length / imageWidth; + + let lastValue = null; + let rectangleStart = 0; + + // for each pixel compute its status based on the pieces + for (let x = 0; x < imageWidth; ++x) { + // find positions in the pieces array + const piecesFrom = x * ratio; + const piecesTo = (x + 1) * ratio; + const piecesToInt = Math.ceil(piecesTo); + + const statusValues = { + [this.STATUS_DOWNLOADING]: 0, + [this.STATUS_DOWNLOADED]: 0 + }; + + // aggregate the status of each piece that contributes to this pixel + for (let p = piecesFrom; p < piecesToInt; ++p) { + const piece = Math.floor(p); + const pieceStart = Math.max(piecesFrom, piece); + const pieceEnd = Math.min(piece + 1, piecesTo); + + const amount = pieceEnd - pieceStart; + const status = pieces[piece]; + + if (status in statusValues) + statusValues[status] += amount; + } + + // normalize to interval [0, 1] + statusValues[this.STATUS_DOWNLOADING] /= ratio; + statusValues[this.STATUS_DOWNLOADED] /= ratio; + + // floats accumulate small errors, so smooth it out by rounding to hundredths place + // this effectively limits each status to a value 1 in 100 + statusValues[this.STATUS_DOWNLOADING] = Math.round(statusValues[this.STATUS_DOWNLOADING] * 100) / 100; + statusValues[this.STATUS_DOWNLOADED] = Math.round(statusValues[this.STATUS_DOWNLOADED] * 100) / 100; + + // float precision sometimes _still_ gives > 1 + statusValues[this.STATUS_DOWNLOADING] = Math.min(statusValues[this.STATUS_DOWNLOADING], 1); + statusValues[this.STATUS_DOWNLOADED] = Math.min(statusValues[this.STATUS_DOWNLOADED], 1); + + if (!lastValue) { + lastValue = statusValues; + } + + // group contiguous colors together and draw as a single rectangle + if ((lastValue[this.STATUS_DOWNLOADING] === statusValues[this.STATUS_DOWNLOADING]) + && (lastValue[this.STATUS_DOWNLOADED] === statusValues[this.STATUS_DOWNLOADED])) { + continue; + } + + const rectangleWidth = x - rectangleStart; + this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); + + lastValue = statusValues; + rectangleStart = x; + } + + // fill a rect at the end of the canvas + if (rectangleStart < imageWidth) { + const rectangleWidth = imageWidth - rectangleStart; + this._drawStatus(ctx, rectangleStart, rectangleWidth, lastValue); + } + } + + _drawStatus(ctx, start, width, statusValues) { + // mix the colors by using transparency and a composite mode + ctx.globalCompositeOperation = 'lighten'; + + if (statusValues[this.STATUS_DOWNLOADING]) { + ctx.globalAlpha = statusValues[this.STATUS_DOWNLOADING]; + ctx.fillStyle = this.downloadingColor; + ctx.fillRect(start, 0, width, ctx.canvas.height); + } + + if (statusValues[this.STATUS_DOWNLOADED]) { + ctx.globalAlpha = statusValues[this.STATUS_DOWNLOADED]; + ctx.fillStyle = this.haveColor; + ctx.fillRect(start, 0, width, ctx.canvas.height); + } + } + + checkForParent(id) { + const obj = document.getElementById(id); + if (!obj) { + return; + } + if (!obj.parentNode) { + return setTimeout(function () { checkForParent(id); }, 1); + } + + this.refresh(); + } +} + +window.qbt.PiecesBar = PiecesBar; + +Object.freeze(window.qbt.PiecesBar);