From e9e41950f7504f00ba564e03352d541b6ffbd216 Mon Sep 17 00:00:00 2001 From: ahjephson Date: Sun, 23 Jun 2024 10:37:25 +0100 Subject: [PATCH] Add partial search implementation and fix bugs --- .../Lantean.QBTBlazor.Test.csproj | 6 +- .../Dialogs/ColumnOptionsDialog.razor | 4 +- .../Dialogs/ColumnOptionsDialog.razor.cs | 28 ++- .../Components/DynamicTable.razor.cs | 2 +- .../Components/FilesTab.razor.cs | 19 +- .../Components/GeneralTab.razor | 4 +- .../Components/GeneralTab.razor.cs | 32 ++- Lantean.QBTMudBlade/Components/Menu.razor | 8 +- Lantean.QBTMudBlade/Components/Menu.razor.cs | 11 - Lantean.QBTMudBlade/Components/PeersTab.razor | 13 +- .../Components/PeersTab.razor.cs | 40 +++- .../Components/TrackersTab.razor | 3 +- .../Components/WebSeedsTab.razor | 3 +- Lantean.QBTMudBlade/DialogHelper.cs | 18 +- Lantean.QBTMudBlade/Extensions.cs | 5 + .../Lantean.QBTMudBlade.csproj | 11 +- .../Layout/LoggedInLayout.razor | 20 +- Lantean.QBTMudBlade/Layout/MainLayout.razor | 2 +- .../Layout/MainLayout.razor.cs | 35 +++- Lantean.QBTMudBlade/Layout/OtherLayout.razor | 19 ++ .../Layout/OtherLayout.razor.cs | 20 ++ Lantean.QBTMudBlade/Models/Peer.cs | 18 +- Lantean.QBTMudBlade/Pages/Blocks.razor | 16 ++ Lantean.QBTMudBlade/Pages/Blocks.razor.cs | 39 ++++ Lantean.QBTMudBlade/Pages/Log.razor | 16 ++ Lantean.QBTMudBlade/Pages/Log.razor.cs | 39 ++++ Lantean.QBTMudBlade/Pages/Login.razor.cs | 8 +- Lantean.QBTMudBlade/Pages/Options.razor | 92 ++++----- Lantean.QBTMudBlade/Pages/Options.razor.cs | 32 ++- Lantean.QBTMudBlade/Pages/Rss.razor | 16 ++ Lantean.QBTMudBlade/Pages/Rss.razor.cs | 39 ++++ Lantean.QBTMudBlade/Pages/Search.razor | 80 ++++++++ Lantean.QBTMudBlade/Pages/Search.razor.cs | 194 ++++++++++++++++++ Lantean.QBTMudBlade/Pages/Statistics.razor | 62 ++++++ Lantean.QBTMudBlade/Pages/Statistics.razor.cs | 39 ++++ .../Pages/TorrentList.razor.cs | 41 +--- Lantean.QBTMudBlade/Services/DataManager.cs | 26 +-- Lantean.QBTMudBlade/readme.md | 7 +- Lantean.QBTMudBlade/wwwroot/css/app.css | 9 + Lantean.QBitTorrentClient/ApiClient.cs | 149 +++++++++++++- Lantean.QBitTorrentClient/IApiClient.cs | 24 ++- Lantean.QBitTorrentClient/MockApiClient.cs | 60 ++++++ .../Models/SearchCategory.cs | 20 ++ .../Models/SearchPlugin.cs | 42 ++++ .../Models/SearchResult.cs | 47 +++++ .../Models/SearchResults.cs | 24 +++ .../Models/SearchStatus.cs | 24 +++ 47 files changed, 1266 insertions(+), 200 deletions(-) create mode 100644 Lantean.QBTMudBlade/Layout/OtherLayout.razor create mode 100644 Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Blocks.razor create mode 100644 Lantean.QBTMudBlade/Pages/Blocks.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Log.razor create mode 100644 Lantean.QBTMudBlade/Pages/Log.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Rss.razor create mode 100644 Lantean.QBTMudBlade/Pages/Rss.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Search.razor create mode 100644 Lantean.QBTMudBlade/Pages/Search.razor.cs create mode 100644 Lantean.QBTMudBlade/Pages/Statistics.razor create mode 100644 Lantean.QBTMudBlade/Pages/Statistics.razor.cs create mode 100644 Lantean.QBitTorrentClient/Models/SearchCategory.cs create mode 100644 Lantean.QBitTorrentClient/Models/SearchPlugin.cs create mode 100644 Lantean.QBitTorrentClient/Models/SearchResult.cs create mode 100644 Lantean.QBitTorrentClient/Models/SearchResults.cs create mode 100644 Lantean.QBitTorrentClient/Models/SearchStatus.cs diff --git a/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj b/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj index 2d22779..377f6d3 100644 --- a/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj +++ b/Lantean.QBTBlazor.Test/Lantean.QBTBlazor.Test.csproj @@ -10,10 +10,10 @@ - + - - + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor index 88a8294..8e6dca6 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor +++ b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor @@ -2,14 +2,14 @@ - + @for (var i = 0; i < Columns.Count; i++) { var column = Columns[i]; var index = i; - + diff --git a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs index 6eee73a..f650bc0 100644 --- a/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs +++ b/Lantean.QBTMudBlade/Components/Dialogs/ColumnOptionsDialog.razor.cs @@ -13,18 +13,32 @@ namespace Lantean.QBTMudBlade.Components.Dialogs [EditorRequired] public List> Columns { get; set; } = default!; + [Parameter] + [EditorRequired] + public HashSet SelectedColumns { get; set; } = default!; + [Parameter] public Dictionary Widths { get; set; } = []; - protected HashSet SelectedColumns { get; set; } = []; + protected HashSet SelectedColumnsInternal { get; set; } = []; protected override void OnParametersSet() { - if (SelectedColumns.Count == 0) + if (SelectedColumnsInternal.Count == 0) { - foreach (var column in Columns.Where(c => c.Enabled)) + if (SelectedColumns.Count != 0) { - SelectedColumns.Add(column.Id); + foreach (var selectedColumn in SelectedColumns) + { + SelectedColumnsInternal.Add(selectedColumn); + } + } + else + { + foreach (var column in Columns.Where(c => c.Enabled)) + { + SelectedColumns.Add(column.Id); + } } } } @@ -33,11 +47,11 @@ namespace Lantean.QBTMudBlade.Components.Dialogs { if (selected) { - SelectedColumns.Add(id); + SelectedColumnsInternal.Add(id); } else { - SelectedColumns.Remove(id); + SelectedColumnsInternal.Remove(id); } } @@ -117,7 +131,7 @@ namespace Lantean.QBTMudBlade.Components.Dialogs protected void Submit(MouseEventArgs args) { - MudDialog.Close(DialogResult.Ok((SelectedColumns, Widths))); + MudDialog.Close(DialogResult.Ok((SelectedColumnsInternal, Widths))); } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Components/DynamicTable.razor.cs b/Lantean.QBTMudBlade/Components/DynamicTable.razor.cs index ba68ada..665151a 100644 --- a/Lantean.QBTMudBlade/Components/DynamicTable.razor.cs +++ b/Lantean.QBTMudBlade/Components/DynamicTable.razor.cs @@ -249,7 +249,7 @@ namespace Lantean.QBTMudBlade.Components public async Task ShowColumnOptionsDialog() { - var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), _columnWidths); + var result = await DialogService.ShowColumnsOptionsDialog(ColumnDefinitions.Where(ColumnFilter).ToList(), SelectedColumns, _columnWidths); if (result == default) { diff --git a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs index 4214eb9..5f8ad5d 100644 --- a/Lantean.QBTMudBlade/Components/FilesTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/FilesTab.razor.cs @@ -19,6 +19,13 @@ namespace Lantean.QBTMudBlade.Components private readonly CancellationTokenSource _timerCancellationToken = new(); private bool _disposedValue; + private List>? _filterDefinitions; + private readonly Dictionary>> _columnRenderFragments = []; + + private string? _previousHash; + private string? _sortColumn; + private SortDirection _sortDirection; + [Parameter] public bool Active { get; set; } @@ -46,21 +53,15 @@ namespace Lantean.QBTMudBlade.Components protected IEnumerable Files => GetFiles(); - private List>? _filterDefinitions; - protected ContentItem? SelectedItem { get; set; } protected string? SearchText { get; set; } public IEnumerable>? Filters { get; set; } - private readonly Dictionary>> _columnRenderFragments = []; - private DynamicTable? Table { get; set; } - private string? _previousHash; - private string? _sortColumn; - private SortDirection _sortDirection; + public FilesTab() { @@ -280,7 +281,7 @@ namespace Lantean.QBTMudBlade.Components } var name = contentItem.GetFileName(); - await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async v => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + v)); + await DialogService.ShowSingleFieldDialog("Rename", "New name", name, async value => await ApiClient.RenameFile(Hash, contentItem.Name, contentItem.Path + value)); } protected void SortColumnChanged(string sortColumn) @@ -396,7 +397,7 @@ namespace Lantean.QBTMudBlade.Components private ReadOnlyCollection GetFiles() { - if (FileList is null) + if (FileList is null || FileList.Values.Count == 0) { return new ReadOnlyCollection([]); } diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor b/Lantean.QBTMudBlade/Components/GeneralTab.razor index 50079d4..25ceb47 100644 --- a/Lantean.QBTMudBlade/Components/GeneralTab.razor +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor @@ -1,4 +1,4 @@ - + Progress Transfer @@ -17,7 +17,7 @@ @DisplayHelpers.Size(Properties?.TotalDownloaded) @DisplayHelpers.Size(Properties?.TotalDownloadedSession, "(", " this session)") - @DisplayHelpers.Size(Properties?.TotalUploaded) @DisplayHelpers.Size(Properties?.TotalUploaded, "(", " this session)") + @DisplayHelpers.Size(Properties?.TotalUploaded) @DisplayHelpers.Size(Properties?.TotalUploaded, "(", " this session)") @DisplayHelpers.Size(Properties?.Seeds) @DisplayHelpers.EmptyIfNull(Properties?.Seeds, "(", " total)") diff --git a/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs index 25d99e0..b9c0bbe 100644 --- a/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/GeneralTab.razor.cs @@ -44,8 +44,23 @@ namespace Lantean.QBTMudBlade.Components return; } - Pieces = await ApiClient.GetTorrentPieceStates(Hash); - Properties = await ApiClient.GetTorrentProperties(Hash); + try + { + Properties = await ApiClient.GetTorrentProperties(Hash); + } + catch (HttpRequestException) + { + return; + } + + try + { + Pieces = await ApiClient.GetTorrentPieceStates(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.NotFound) + { + Pieces = []; + } await InvokeAsync(StateHasChanged); } @@ -67,14 +82,23 @@ namespace Lantean.QBTMudBlade.Components { try { - Pieces = await ApiClient.GetTorrentPieceStates(Hash); Properties = await ApiClient.GetTorrentProperties(Hash); } - catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden || exception.StatusCode == HttpStatusCode.NotFound) + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) { _timerCancellationToken.CancelIfNotDisposed(); return; } + + try + { + Pieces = await ApiClient.GetTorrentPieceStates(Hash); + } + catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.NotFound) + { + Pieces = []; + return; + } } await InvokeAsync(StateHasChanged); diff --git a/Lantean.QBTMudBlade/Components/Menu.razor b/Lantean.QBTMudBlade/Components/Menu.razor index cc94784..843a267 100644 --- a/Lantean.QBTMudBlade/Components/Menu.razor +++ b/Lantean.QBTMudBlade/Components/Menu.razor @@ -1,7 +1,11 @@  - Statistics + Statistics + Search + RSS + Execution Log + Blocked IPs - Settings + Settings Reset Web UI Logout diff --git a/Lantean.QBTMudBlade/Components/Menu.razor.cs b/Lantean.QBTMudBlade/Components/Menu.razor.cs index 78e3e85..94b86d8 100644 --- a/Lantean.QBTMudBlade/Components/Menu.razor.cs +++ b/Lantean.QBTMudBlade/Components/Menu.razor.cs @@ -20,7 +20,6 @@ namespace Lantean.QBTMudBlade.Components { var preferences = new UpdatePreferences { - AlternativeWebuiPath = null, AlternativeWebuiEnabled = false, }; @@ -29,16 +28,6 @@ namespace Lantean.QBTMudBlade.Components NavigationManager.NavigateTo("/", true); } - protected void Settings() - { - NavigationManager.NavigateTo("/options"); - } - - protected void Statistics() - { - NavigationManager.NavigateTo("/statistics"); - } - protected async Task Logout() { await DialogService.ShowConfirmDialog("Logout?", "Are you sure you want to logout?", async () => diff --git a/Lantean.QBTMudBlade/Components/PeersTab.razor b/Lantean.QBTMudBlade/Components/PeersTab.razor index 75363da..e1b3519 100644 --- a/Lantean.QBTMudBlade/Components/PeersTab.razor +++ b/Lantean.QBTMudBlade/Components/PeersTab.razor @@ -12,9 +12,13 @@ HorizontalScrollbar="true" Virtualize="true" AllowUnsorted="false" - SelectOnRowClick="false"> + SelectOnRowClick="false" + Class="details-list"> - Country/Region + @if (ShowFlags) + { + Country/Region + } IP Port Connection @@ -29,7 +33,10 @@ Files - + @if (ShowFlags) + { + + } @context.IPAddress @context.Port @context.Connection diff --git a/Lantean.QBTMudBlade/Components/PeersTab.razor.cs b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs index 999be96..4a38b9c 100644 --- a/Lantean.QBTMudBlade/Components/PeersTab.razor.cs +++ b/Lantean.QBTMudBlade/Components/PeersTab.razor.cs @@ -9,6 +9,10 @@ namespace Lantean.QBTMudBlade.Components public partial class PeersTab : IAsyncDisposable { private bool _disposedValue; + protected string? _oldHash; + private int _requestId = 0; + private readonly CancellationTokenSource _timerCancellationToken = new(); + private bool? _showFlags; [Parameter, EditorRequired] public string? Hash { get; set; } @@ -19,6 +23,9 @@ namespace Lantean.QBTMudBlade.Components [CascadingParameter] public int RefreshInterval { get; set; } + [CascadingParameter] + public QBitTorrentClient.Models.Preferences? Preferences { get; set; } + [Inject] protected IApiClient ApiClient { get; set; } = default!; @@ -29,8 +36,7 @@ namespace Lantean.QBTMudBlade.Components protected IEnumerable Peers => PeerList?.Peers.Select(p => p.Value) ?? []; - private int _requestId = 0; - private readonly CancellationTokenSource _timerCancellationToken = new(); + protected bool ShowFlags => _showFlags.GetValueOrDefault(); protected override async Task OnParametersSetAsync() { @@ -44,9 +50,33 @@ namespace Lantean.QBTMudBlade.Components return; } - var torrentPeers = await ApiClient.GetTorrentPeersData(Hash, _requestId); - PeerList = DataManager.CreatePeerList(torrentPeers); - _requestId = torrentPeers.RequestId; + if (Hash != _oldHash) + { + _oldHash = Hash; + _requestId = 0; + PeerList = null; + } + + var peers = await ApiClient.GetTorrentPeersData(Hash, _requestId); + if (PeerList is null || peers.FullUpdate) + { + PeerList = DataManager.CreatePeerList(peers); + } + else + { + DataManager.MergeTorrentPeers(peers, PeerList); + } + _requestId = peers.RequestId; + + if (Preferences is not null && _showFlags is null) + { + _showFlags = Preferences.ResolvePeerCountries; + } + + if (peers.ShowFlags.HasValue) + { + _showFlags = peers.ShowFlags.Value; + } await InvokeAsync(StateHasChanged); } diff --git a/Lantean.QBTMudBlade/Components/TrackersTab.razor b/Lantean.QBTMudBlade/Components/TrackersTab.razor index c536701..1a107ff 100644 --- a/Lantean.QBTMudBlade/Components/TrackersTab.razor +++ b/Lantean.QBTMudBlade/Components/TrackersTab.razor @@ -12,7 +12,8 @@ HorizontalScrollbar="true" Virtualize="true" AllowUnsorted="false" - SelectOnRowClick="false"> + SelectOnRowClick="false" + Class="details-list"> Tier URL diff --git a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor index cdabc22..b4b0a50 100644 --- a/Lantean.QBTMudBlade/Components/WebSeedsTab.razor +++ b/Lantean.QBTMudBlade/Components/WebSeedsTab.razor @@ -12,7 +12,8 @@ HorizontalScrollbar="true" Virtualize="true" AllowUnsorted="false" - SelectOnRowClick="false"> + SelectOnRowClick="false" + Class="details-list"> URL diff --git a/Lantean.QBTMudBlade/DialogHelper.cs b/Lantean.QBTMudBlade/DialogHelper.cs index 83afb2a..7f24d2e 100644 --- a/Lantean.QBTMudBlade/DialogHelper.cs +++ b/Lantean.QBTMudBlade/DialogHelper.cs @@ -149,6 +149,18 @@ namespace Lantean.QBTMudBlade return tags; } + public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content) + { + var parameters = new DialogParameters + { + { nameof(ConfirmDialog.Content), content } + }; + var result = await dialogService.ShowAsync(title, parameters, ConfirmDialogOptions); + + var dialogResult = await result.Result; + return !dialogResult.Canceled; + } + public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func onSuccess) { var parameters = new DialogParameters @@ -166,7 +178,7 @@ namespace Lantean.QBTMudBlade await onSuccess(); } - public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, System.Action onSuccess) + public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Action onSuccess) { await ShowConfirmDialog(dialogService, title, content, () => { @@ -269,11 +281,13 @@ namespace Lantean.QBTMudBlade return (List>?)dialogResult.Data; } - public static async Task<(HashSet SelectedColumns, Dictionary ColumnWidths)> ShowColumnsOptionsDialog(this IDialogService dialogService, List> columnDefinitions, Dictionary widths) + public static async Task<(HashSet SelectedColumns, Dictionary ColumnWidths)> ShowColumnsOptionsDialog(this IDialogService dialogService, List> columnDefinitions, HashSet selectedColumns, Dictionary widths) { var parameters = new DialogParameters { { nameof(ColumnOptionsDialog.Columns), columnDefinitions }, + { nameof(ColumnOptionsDialog.SelectedColumns), selectedColumns }, + { nameof(ColumnOptionsDialog.Widths), widths }, }; var reference = await dialogService.ShowAsync>("Column Options", parameters, FormDialogOptions); diff --git a/Lantean.QBTMudBlade/Extensions.cs b/Lantean.QBTMudBlade/Extensions.cs index e5b3932..87477a1 100644 --- a/Lantean.QBTMudBlade/Extensions.cs +++ b/Lantean.QBTMudBlade/Extensions.cs @@ -57,6 +57,11 @@ namespace Lantean.QBTMudBlade public static bool MetaDownloaded(this Torrent torrent) { + if (torrent is null) + { + return false; + } + return !(torrent.State == "metaDL" || torrent.State == "forcedMetaDL" || torrent.TotalSize == -1); } } diff --git a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj index 55dba73..46ebeb3 100644 --- a/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj +++ b/Lantean.QBTMudBlade/Lantean.QBTMudBlade.csproj @@ -11,11 +11,11 @@ - - + + - - + + @@ -26,6 +26,9 @@ true + + true + diff --git a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor index 61557a4..4f2b4ee 100644 --- a/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor +++ b/Lantean.QBTMudBlade/Layout/LoggedInLayout.razor @@ -34,31 +34,31 @@ - + @if (MainData?.LostConnection == true) { - qBittorrent client is not reachable + qBittorrent client is not reachable } - @DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ") + @DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ") - DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes + DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes @{ var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus); } - + - + - - + + @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s") @DisplayHelpers.Size(MainData?.ServerState.DownloadInfoData, "(", ")") - - + + @DisplayHelpers.Size(MainData?.ServerState.UploadInfoSpeed, null, "/s") @DisplayHelpers.Size(MainData?.ServerState.UploadInfoData, "(", ")") diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor b/Lantean.QBTMudBlade/Layout/MainLayout.razor index b6f72b5..e9b0db1 100644 --- a/Lantean.QBTMudBlade/Layout/MainLayout.razor +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor @@ -19,7 +19,7 @@ } - + @if (ShowMenu) { diff --git a/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs index 2642c5b..981fa21 100644 --- a/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs +++ b/Lantean.QBTMudBlade/Layout/MainLayout.razor.cs @@ -1,13 +1,18 @@ -using Lantean.QBitTorrentClient; +using Blazored.LocalStorage; +using Lantean.QBitTorrentClient; using Lantean.QBTMudBlade.Components; using Microsoft.AspNetCore.Components; using MudBlazor; using MudBlazor.Services; +using static MudBlazor.Colors; namespace Lantean.QBTMudBlade.Layout { public partial class MainLayout : IBrowserViewportObserver, IAsyncDisposable { + private const string _isDarkModeStorageKey = "MainLayout.IsDarkMode"; + private const string _drawerOpenStorageKey = "MainLayout.DrawerOpen"; + private bool _disposedValue; [Inject] @@ -19,6 +24,9 @@ namespace Lantean.QBTMudBlade.Layout [Inject] private IApiClient ApiClient { get; set; } = default!; + [Inject] + protected ILocalStorageService LocalStorage { get; set; } = default!; + protected bool DrawerOpen { get; set; } = true; protected bool ErrorDrawerOpen { get; set; } = false; @@ -47,9 +55,10 @@ namespace Lantean.QBTMudBlade.Layout Theme.Typography.Default.FontFamily = ["Nunito Sans"]; } - protected void ToggleDrawer() + protected async Task ToggleDrawer() { DrawerOpen = !DrawerOpen; + await LocalStorage.SetItemAsync(_drawerOpenStorageKey, DrawerOpen); } protected override async Task OnParametersSetAsync() @@ -58,13 +67,27 @@ namespace Lantean.QBTMudBlade.Layout { ShowMenu = await ApiClient.CheckAuthState(); } + + var drawerOpen = await LocalStorage.GetItemAsync(_drawerOpenStorageKey); + if (drawerOpen is not null) + { + DrawerOpen = drawerOpen.Value; + } } protected override async Task OnAfterRenderAsync(bool firstRender) { if (firstRender) { - IsDarkMode = await MudThemeProvider.GetSystemPreference(); + var isDarkMode = await LocalStorage.GetItemAsync(_isDarkModeStorageKey); + if (isDarkMode is null) + { + IsDarkMode = await MudThemeProvider.GetSystemPreference(); + } + else + { + IsDarkMode = isDarkMode.Value; + } await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged); await BrowserViewportService.SubscribeAsync(this, fireImmediately: true); await InvokeAsync(StateHasChanged); @@ -114,6 +137,12 @@ namespace Lantean.QBTMudBlade.Layout } } + protected async Task DarkModeChanged(bool value) + { + IsDarkMode = value; + await LocalStorage.SetItemAsync(_isDarkModeStorageKey, value); + } + public async ValueTask DisposeAsync() { // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method diff --git a/Lantean.QBTMudBlade/Layout/OtherLayout.razor b/Lantean.QBTMudBlade/Layout/OtherLayout.razor new file mode 100644 index 0000000..e75f877 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/OtherLayout.razor @@ -0,0 +1,19 @@ +@inherits LayoutComponentBase +@layout LoggedInLayout + + + + Back + + Statistics + Search + RSS + Execution Log + Blocked IPs + + Settings + + + + @Body + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs b/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs new file mode 100644 index 0000000..273a3e9 --- /dev/null +++ b/Lantean.QBTMudBlade/Layout/OtherLayout.razor.cs @@ -0,0 +1,20 @@ +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Layout +{ + public partial class OtherLayout + { + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Models/Peer.cs b/Lantean.QBTMudBlade/Models/Peer.cs index f892d5e..961cc0e 100644 --- a/Lantean.QBTMudBlade/Models/Peer.cs +++ b/Lantean.QBTMudBlade/Models/Peer.cs @@ -3,12 +3,12 @@ public class Peer { public Peer( - string ip, + string key, string client, string clientId, string connection, - string country, - string countryCode, + string? country, + string? countryCode, long downloaded, long downloadSpeed, string files, @@ -21,7 +21,7 @@ long uploaded, long uploadSpeed) { - IP = ip; + Key = key; Client = client; ClientId = clientId; Connection = connection; @@ -40,12 +40,12 @@ UploadSpeed = uploadSpeed; } - public string IP { get; } + public string Key { get; } public string Client { get; set; } public string ClientId { get; set; } public string Connection { get; set; } - public string Country { get; set; } - public string CountryCode { get; set; } + public string? Country { get; set; } + public string? CountryCode { get; set; } public long Downloaded { get; set; } public long DownloadSpeed { get; set; } public string Files { get; set; } @@ -61,12 +61,12 @@ public override bool Equals(object? obj) { if (obj is null) return false; - return ((Peer)obj).IP == IP; + return ((Peer)obj).Key == Key; } public override int GetHashCode() { - return IP.GetHashCode(); + return Key.GetHashCode(); } } } \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Blocks.razor b/Lantean.QBTMudBlade/Pages/Blocks.razor new file mode 100644 index 0000000..bee287f --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Blocks.razor @@ -0,0 +1,16 @@ +@page "/blocks" +@layout OtherLayout + + + @if (!DrawerOpen) + { + + + } + + Blocked IPs + + + +

Coming soon.

+
diff --git a/Lantean.QBTMudBlade/Pages/Blocks.razor.cs b/Lantean.QBTMudBlade/Pages/Blocks.razor.cs new file mode 100644 index 0000000..a6002be --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Blocks.razor.cs @@ -0,0 +1,39 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Blocks + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected ServerState? ServerState => MainData?.ServerState; + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Log.razor b/Lantean.QBTMudBlade/Pages/Log.razor new file mode 100644 index 0000000..3c0abf2 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Log.razor @@ -0,0 +1,16 @@ +@page "/log" +@layout OtherLayout + + + @if (!DrawerOpen) + { + + + } + + Execution Log + + + +

Coming soon.

+
diff --git a/Lantean.QBTMudBlade/Pages/Log.razor.cs b/Lantean.QBTMudBlade/Pages/Log.razor.cs new file mode 100644 index 0000000..44e7f22 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Log.razor.cs @@ -0,0 +1,39 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Log + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected ServerState? ServerState => MainData?.ServerState; + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Login.razor.cs b/Lantean.QBTMudBlade/Pages/Login.razor.cs index 82fc4a3..899ebd5 100644 --- a/Lantean.QBTMudBlade/Pages/Login.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Login.razor.cs @@ -19,9 +19,9 @@ namespace Lantean.QBTMudBlade.Pages protected string? ApiError { get; set; } - protected async Task LoginClick(EditContext context) + protected Task LoginClick(EditContext context) { - await DoLogin(Model.Username, Model.Password); + return DoLogin(Model.Username, Model.Password); } private async Task DoLogin(string username, string password) @@ -47,9 +47,9 @@ namespace Lantean.QBTMudBlade.Pages } #if DEBUG - protected override async Task OnInitializedAsync() + protected override Task OnInitializedAsync() { - await DoLogin("admin", "MX6r8xzTP"); + return DoLogin("admin", "6K3mtPNnQ"); } #endif } diff --git a/Lantean.QBTMudBlade/Pages/Options.razor b/Lantean.QBTMudBlade/Pages/Options.razor index 80ab5c3..f9fd163 100644 --- a/Lantean.QBTMudBlade/Pages/Options.razor +++ b/Lantean.QBTMudBlade/Pages/Options.razor @@ -1,55 +1,41 @@ -@page "/options" -@layout LoggedInLayout +@page "/settings" +@layout OtherLayout - - - Back - - Behaviour - Downloads - Connection - Speed - BitTorrent - RSS - Web UI - Advanced - - - - - @if (!DrawerOpen) - { - - - } - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file + + @if (!DrawerOpen) + { + + + } + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Options.razor.cs b/Lantean.QBTMudBlade/Pages/Options.razor.cs index e6f02c5..16184cc 100644 --- a/Lantean.QBTMudBlade/Pages/Options.razor.cs +++ b/Lantean.QBTMudBlade/Pages/Options.razor.cs @@ -3,6 +3,7 @@ using Lantean.QBitTorrentClient.Models; using Lantean.QBTMudBlade.Components.Options; using Lantean.QBTMudBlade.Services; using Microsoft.AspNetCore.Components; +using Microsoft.AspNetCore.Components.Routing; using MudBlazor; namespace Lantean.QBTMudBlade.Pages @@ -62,18 +63,39 @@ namespace Lantean.QBTMudBlade.Pages UpdatePreferences = DataManager.MergePreferences(UpdatePreferences, preferences); } - protected async Task NavigateBack() + protected async Task ValidateExit(LocationChangingContext context) { if (UpdatePreferences is null) { - NavigationManager.NavigateTo("/"); return; } - await DialogService.ShowConfirmDialog( + var exit = await DialogService.ShowConfirmDialog( "Unsaved Changed", - "Are you sure you want to leave without saving your changes?", - () => NavigationManager.NavigateTo("/")); + "Are you sure you want to leave without saving your changes?"); + + if (!exit) + { + context.PreventNavigation(); + } + } + + protected async Task NavigateBack() + { + //if (UpdatePreferences is null) + //{ + // NavigationManager.NavigateTo("/"); + // return; + //} + + //await DialogService.ShowConfirmDialog( + // "Unsaved Changed", + // "Are you sure you want to leave without saving your changes?", + // () => NavigationManager.NavigateTo("/")); + + await Task.CompletedTask; + + NavigationManager.NavigateTo("/"); } protected async Task Undo() diff --git a/Lantean.QBTMudBlade/Pages/Rss.razor b/Lantean.QBTMudBlade/Pages/Rss.razor new file mode 100644 index 0000000..565fcd5 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Rss.razor @@ -0,0 +1,16 @@ +@page "/rss" +@layout OtherLayout + + + @if (!DrawerOpen) + { + + + } + + RSS + + + +

Coming soon.

+
diff --git a/Lantean.QBTMudBlade/Pages/Rss.razor.cs b/Lantean.QBTMudBlade/Pages/Rss.razor.cs new file mode 100644 index 0000000..f7d98ef --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Rss.razor.cs @@ -0,0 +1,39 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Rss + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected ServerState? ServerState => MainData?.ServerState; + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Search.razor b/Lantean.QBTMudBlade/Pages/Search.razor new file mode 100644 index 0000000..47de928 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Search.razor @@ -0,0 +1,80 @@ +@page "/search" +@layout OtherLayout + + + @if (!DrawerOpen) + { + + + } + + Search + + + + + + + + + + + @foreach (var (value, name) in Categories) + { + @name + if (value == "all") + { + + } + } + + + + + All + + @foreach (var (value, name) in Plugins) + { + @name + } + + + + @(_searchId is null ? "Search" : "Stop") + + + + + + + + + Name + Size + Seeders + Leechers + Search engine + + + @context.FileName + @DisplayHelpers.Size(context.FileSize) + @context.Seeders + @context.Leechers + @context.SiteUrl + + \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Search.razor.cs b/Lantean.QBTMudBlade/Pages/Search.razor.cs new file mode 100644 index 0000000..effa0ad --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Search.razor.cs @@ -0,0 +1,194 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Search : IDisposable + { + private IReadOnlyList? _plugins; + private int? _searchId; + private bool _disposedValue; + private readonly CancellationTokenSource _timerCancellationToken = new(); + private int _refreshInterval = 1500; + + private QBitTorrentClient.Models.SearchResults? _searchResults; + + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected ServerState? ServerState => MainData?.ServerState; + + protected string? SearchText { get; set; } + + protected string SelectedPlugin { get; set; } = "all"; + + protected string SelectedCategory { get; set; } = "all"; + + protected Dictionary Plugins => _plugins is null ? [] : _plugins.ToDictionary(a => a.Name, a => a.FullName); + + protected Dictionary Categories => GetCategories(SelectedPlugin); + + protected IEnumerable? Results => _searchResults?.Results; + + protected override async Task OnInitializedAsync() + { + _plugins = await ApiClient.GetSearchPlugins(); + } + + protected override async Task OnAfterRenderAsync(bool firstRender) + { + if (firstRender) + { + using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(_refreshInterval))) + { + while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync()) + { + if (_searchId is not null) + { + try + { + _searchResults = await ApiClient.GetSearchResults(_searchId.Value); + + if (_searchResults.Status == "Stopped") + { + await ApiClient.DeleteSearch(_searchId.Value); + _searchId = null; + } + } + catch (HttpRequestException) + { + if (MainData is not null) + { + MainData.LostConnection = true; + } + _searchId = null; + } + + await InvokeAsync(StateHasChanged); + } + } + } + } + } + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + + protected void SearchTextChanged(string value) + { + SearchText = value; + } + + protected void SelectedCategoryChanged(string value) + { + SelectedCategory = value; + } + + protected void SelectedPluginChanged(string value) + { + SelectedPlugin = value; + } + + private Dictionary GetCategories(string plugin) + { + if (_plugins is null) + { + return []; + } + + if (plugin == "all") + { + return _plugins.SelectMany(i => i.SupportedCategories).Distinct().ToDictionary(a => a.Id, a => a.Name); + } + + var pluginItem = _plugins.FirstOrDefault(p => p.Name == plugin); + if (pluginItem is null) + { + return []; + } + + return pluginItem.SupportedCategories.ToDictionary(a => a.Id, a => a.Name); + } + + protected async Task DoSearch() + { + if (_searchId is null) + { + if (string.IsNullOrEmpty(SearchText)) + { + return; + } + + _searchResults = null; + _searchId = await ApiClient.StartSearch(SearchText, [SelectedPlugin], SelectedCategory); + } + else + { + try + { + var status = await ApiClient.GetSearchStatus(_searchId.Value); + + if (status is not null) + { + if (status.Status == "Running") + { + await ApiClient.StopSearch(_searchId.Value); + } + + await ApiClient.DeleteSearch(_searchId.Value); + + _searchId = null; + } + } + catch (HttpRequestException exception) when (exception.StatusCode == System.Net.HttpStatusCode.NotFound) + { + _searchId = null; + } + } + } + + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + _timerCancellationToken.Cancel(); + _timerCancellationToken.Dispose(); + } + + _disposedValue = true; + } + } + + public void Dispose() + { + // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/Statistics.razor b/Lantean.QBTMudBlade/Pages/Statistics.razor new file mode 100644 index 0000000..a8ce5e8 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Statistics.razor @@ -0,0 +1,62 @@ +@page "/statistics" +@layout OtherLayout + + + @if (!DrawerOpen) + { + + + } + + Statistics + + + + User statistics + + + @DisplayHelpers.Size(ServerState?.AllTimeUploaded) + + + @DisplayHelpers.Size(ServerState?.AllTimeDownloaded) + + + @DisplayHelpers.EmptyIfNull(ServerState?.GlobalRatio, format: "0.00") + + + @DisplayHelpers.Size(ServerState?.TotalWastedSession) + + + @DisplayHelpers.EmptyIfNull(ServerState?.TotalPeerConnections) + + + + Cache statistics + + + @DisplayHelpers.Percentage(ServerState?.ReadCacheHits) + + + @DisplayHelpers.Size(ServerState?.TotalBuffersSize) + + + + Performance statistics + + + @DisplayHelpers.Percentage(ServerState?.WriteCacheOverload) + + + @DisplayHelpers.Percentage(ServerState?.ReadCacheOverload) + + + @DisplayHelpers.EmptyIfNull(ServerState?.QueuedIOJobs) + + + @DisplayHelpers.EmptyIfNull(ServerState?.AverageTimeQueue, suffix: "ms") + + + @DisplayHelpers.Size(ServerState?.TotalQueuedSize) + + + diff --git a/Lantean.QBTMudBlade/Pages/Statistics.razor.cs b/Lantean.QBTMudBlade/Pages/Statistics.razor.cs new file mode 100644 index 0000000..15028e7 --- /dev/null +++ b/Lantean.QBTMudBlade/Pages/Statistics.razor.cs @@ -0,0 +1,39 @@ +using Lantean.QBitTorrentClient; +using Lantean.QBTMudBlade.Models; +using Microsoft.AspNetCore.Components; +using MudBlazor; + +namespace Lantean.QBTMudBlade.Pages +{ + public partial class Statistics + { + [Inject] + protected IApiClient ApiClient { get; set; } = default!; + + [Inject] + protected IDialogService DialogService { get; set; } = default!; + + [Inject] + protected NavigationManager NavigationManager { get; set; } = default!; + + [CascadingParameter] + public MainData? MainData { get; set; } + + [CascadingParameter(Name = "DrawerOpen")] + public bool DrawerOpen { get; set; } + + [Parameter] + public string? Hash { get; set; } + + protected int ActiveTab { get; set; } = 0; + + protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; + + protected ServerState? ServerState => MainData?.ServerState; + + protected void NavigateBack() + { + NavigationManager.NavigateTo("/"); + } + } +} \ No newline at end of file diff --git a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs index f019ad4..0650ba9 100644 --- a/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs +++ b/Lantean.QBTMudBlade/Pages/TorrentList.razor.cs @@ -16,9 +16,6 @@ namespace Lantean.QBTMudBlade.Pages [Inject] protected IDialogService DialogService { get; set; } = default!; - [Inject] - protected ILocalStorageService LocalStorage { get; set; } = default!; - [Inject] protected NavigationManager NavigationManager { get; set; } = default!; @@ -66,37 +63,6 @@ namespace Lantean.QBTMudBlade.Pages await SearchTermChanged.InvokeAsync(SearchText); } - protected async Task PauseTorrents() - { - await ApiClient.PauseTorrents(GetSelectedTorrents()); - - SelectedItems.Clear(); - await InvokeAsync(StateHasChanged); - } - - protected async Task ResumeTorrents() - { - await ApiClient.ResumeTorrents(GetSelectedTorrents()); - - SelectedItems.Clear(); - await InvokeAsync(StateHasChanged); - } - - protected async Task RemoveTorrents() - { - var reference = await DialogService.ShowAsync("Remove torrent(s)?"); - var result = await reference.Result; - if (result.Canceled) - { - return; - } - - await ApiClient.DeleteTorrents(GetSelectedTorrents(), (bool)result.Data); - - SelectedItems.Clear(); - await InvokeAsync(StateHasChanged); - } - protected async Task AddTorrentFile() { await DialogService.InvokeAddTorrentFileDialog(ApiClient); @@ -130,11 +96,6 @@ namespace Lantean.QBTMudBlade.Pages return []; } - protected void Options() - { - NavigationManager.NavigateTo("/options"); - } - public async Task ColumnOptions() { if (Table is null) @@ -186,7 +147,7 @@ namespace Lantean.QBTMudBlade.Pages CreateColumnDefinition("Remaining", t => t.AmountLeft, t => DisplayHelpers.Size(t.AmountLeft), enabled: false), CreateColumnDefinition("Time Active", t => t.TimeActive, t => DisplayHelpers.Duration(t.TimeActive), enabled: false), CreateColumnDefinition("Save path", t => t.SavePath, enabled: false), - CreateColumnDefinition("Completed", t => t.Completed, t => DisplayHelpers.DateTime(t.Completed), enabled: false), + CreateColumnDefinition("Completed", t => t.Completed, t => DisplayHelpers.Size(t.Completed), enabled: false), CreateColumnDefinition("Ratio Limit", t => t.RatioLimit, t => t.Ratio.ToString("0.00"), enabled: false), CreateColumnDefinition("Last Seen Complete", t => t.SeenComplete, t => DisplayHelpers.DateTime(t.SeenComplete), enabled: false), CreateColumnDefinition("Last Activity", t => t.LastActivity, t => DisplayHelpers.DateTime(t.LastActivity), enabled: false), diff --git a/Lantean.QBTMudBlade/Services/DataManager.cs b/Lantean.QBTMudBlade/Services/DataManager.cs index 7a14fd9..c06e2fe 100644 --- a/Lantean.QBTMudBlade/Services/DataManager.cs +++ b/Lantean.QBTMudBlade/Services/DataManager.cs @@ -11,11 +11,11 @@ namespace Lantean.QBTMudBlade.Services var peers = new Dictionary(); if (torrentPeers.Peers is not null) { - foreach (var (ip, peer) in torrentPeers.Peers) + foreach (var (key, peer) in torrentPeers.Peers) { - var newPeer = CreatePeer(ip, peer); + var newPeer = CreatePeer(key, peer); - peers[ip] = newPeer; + peers[key] = newPeer; } } @@ -373,20 +373,20 @@ namespace Lantean.QBTMudBlade.Services { if (torrentPeers.PeersRemoved is not null) { - foreach (var peer in torrentPeers.PeersRemoved) + foreach (var key in torrentPeers.PeersRemoved) { - peerList.Peers.Remove(peer); + peerList.Peers.Remove(key); } } if (torrentPeers.Peers is not null) { - foreach (var (ip, peer) in torrentPeers.Peers) + foreach (var (key, peer) in torrentPeers.Peers) { - if (!peerList.Peers.TryGetValue(ip, out var existingPeer)) + if (!peerList.Peers.TryGetValue(key, out var existingPeer)) { - var newPeer = CreatePeer(ip, peer); - peerList.Peers.Add(ip, newPeer); + var newPeer = CreatePeer(key, peer); + peerList.Peers.Add(key, newPeer); } else { @@ -421,15 +421,15 @@ namespace Lantean.QBTMudBlade.Services return new Category(category.Name, category.SavePath!); } - private static Peer CreatePeer(string ip, QBitTorrentClient.Models.Peer peer) + private static Peer CreatePeer(string key, QBitTorrentClient.Models.Peer peer) { return new Peer( - ip, + key, peer.Client!, peer.ClientId!, peer.Connection!, - peer.Country!, - peer.CountryCode!, + peer.Country, + peer.CountryCode, peer.Downloaded!.Value, peer.DownloadSpeed!.Value, peer.Files!, diff --git a/Lantean.QBTMudBlade/readme.md b/Lantean.QBTMudBlade/readme.md index 5ba4be6..52569dd 100644 --- a/Lantean.QBTMudBlade/readme.md +++ b/Lantean.QBTMudBlade/readme.md @@ -2,8 +2,9 @@ ## To-Do -- Files -> Context menu alternative -- Details -> Fixed height - Rename multiple files dialog - RSS feeds and dialogs -- Search and dialogs \ No newline at end of file +- Search and dialogs (partially implemented) +- Execution Log +- Blocked IPs +- About \ No newline at end of file diff --git a/Lantean.QBTMudBlade/wwwroot/css/app.css b/Lantean.QBTMudBlade/wwwroot/css/app.css index 17ed134..58330be 100644 --- a/Lantean.QBTMudBlade/wwwroot/css/app.css +++ b/Lantean.QBTMudBlade/wwwroot/css/app.css @@ -161,4 +161,13 @@ td.no-wrap { .file-list .mud-table-container { height: calc(100vh - 245px); +} + +.details-list .mud-table-container { + height: calc(100vh - 200px); +} + +.details-tab-contents { + height: calc(100vh - 200px); + overflow: auto; } \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/ApiClient.cs b/Lantean.QBitTorrentClient/ApiClient.cs index a508060..fd12862 100644 --- a/Lantean.QBitTorrentClient/ApiClient.cs +++ b/Lantean.QBitTorrentClient/ApiClient.cs @@ -1,4 +1,5 @@ using Lantean.QBitTorrentClient.Models; +using System.Collections.Generic; using System.Net; using System.Net.Http.Json; using System.Text.Json; @@ -947,7 +948,153 @@ namespace Lantean.QBitTorrentClient #region Search - // not implementing Search right now + + + public async Task StartSearch(string pattern, IEnumerable plugins, string category = "all") + { + var content = new FormUrlEncodedBuilder() + .Add("pattern", pattern) + .AddPipeSeparated("plugins", plugins) + .Add("category", category) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/start", content); + + response.EnsureSuccessStatusCode(); + + var obj = await GetJson>(response.Content); + + return obj["id"].GetInt32(); + } + + public async Task StopSearch(int id) + { + var content = new FormUrlEncodedBuilder() + .Add("id", id) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/stop", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task GetSearchStatus(int id) + { + var query = new QueryBuilder(); + query.Add("id", id); + + var response = await _httpClient.GetAsync($"search/status{query}"); + if (response.StatusCode == HttpStatusCode.NotFound) + { + return null; + } + + response.EnsureSuccessStatusCode(); + + return (await GetJsonList(response.Content)).FirstOrDefault(); + } + + public async Task> GetSearchesStatus() + { + var response = await _httpClient.GetAsync($"search/status"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task GetSearchResults(int id, int? limit = null, int? offset = null) + { + var query = new QueryBuilder(); + query.Add("id", id); + if (limit is not null) + { + query.Add("limit", limit.Value); + } + if (offset is not null) + { + query.Add("offset", offset.Value); + } + + var response = await _httpClient.GetAsync($"search/results{query}"); + + response.EnsureSuccessStatusCode(); + + return await GetJson(response.Content); + } + + public async Task DeleteSearch(int id) + { + var content = new FormUrlEncodedBuilder() + .Add("id", id) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/delete", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task> GetSearchPlugins() + { + var response = await _httpClient.GetAsync($"search/plugins"); + + response.EnsureSuccessStatusCode(); + + return await GetJsonList(response.Content); + } + + public async Task InstallSearchPlugins(params string[] sources) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("sources", sources) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/installPlugin", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task UninstallSearchPlugins(params string[] names) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("names", names) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/uninstallPlugin", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task EnableSearchPlugins(params string[] names) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("names", names) + .Add("enabble", true) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/enablePlugin", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task DisableSearchPlugins(params string[] names) + { + var content = new FormUrlEncodedBuilder() + .AddPipeSeparated("names", names) + .Add("enabble", false) + .ToFormUrlEncodedContent(); + + var response = await _httpClient.PostAsync("search/enablePlugin", content); + + response.EnsureSuccessStatusCode(); + } + + public async Task UpdateSearchPlugins() + { + var response = await _httpClient.PostAsync("search/updatePlugins", null); + + response.EnsureSuccessStatusCode(); + } #endregion Search diff --git a/Lantean.QBitTorrentClient/IApiClient.cs b/Lantean.QBitTorrentClient/IApiClient.cs index 7499e69..daf5da9 100644 --- a/Lantean.QBitTorrentClient/IApiClient.cs +++ b/Lantean.QBitTorrentClient/IApiClient.cs @@ -178,7 +178,29 @@ namespace Lantean.QBitTorrentClient #region Search - // not implementing Search right now + Task StartSearch(string pattern, IEnumerable plugins, string category = "all"); + + Task StopSearch(int id); + + Task GetSearchStatus(int id); + + Task> GetSearchesStatus(); + + Task GetSearchResults(int id, int? limit = null, int? offset = null); + + Task DeleteSearch(int id); + + Task> GetSearchPlugins(); + + Task InstallSearchPlugins(params string[] sources); + + Task UninstallSearchPlugins(params string[] names); + + Task EnableSearchPlugins(params string[] names); + + Task DisableSearchPlugins(params string[] names); + + Task UpdateSearchPlugins(); #endregion Search } diff --git a/Lantean.QBitTorrentClient/MockApiClient.cs b/Lantean.QBitTorrentClient/MockApiClient.cs index c786489..9a6209d 100644 --- a/Lantean.QBitTorrentClient/MockApiClient.cs +++ b/Lantean.QBitTorrentClient/MockApiClient.cs @@ -378,5 +378,65 @@ namespace Lantean.QBitTorrentClient { return _apiClient.ToggleSequentialDownload(all, hashes); } + + public Task StartSearch(string pattern, IEnumerable plugins, string category = "all") + { + return _apiClient.StartSearch(pattern, plugins, category); + } + + public Task StopSearch(int id) + { + return _apiClient.StopSearch(id); + } + + public Task GetSearchStatus(int id) + { + return _apiClient.GetSearchStatus(id); + } + + public Task> GetSearchesStatus() + { + return _apiClient.GetSearchesStatus(); + } + + public Task GetSearchResults(int id, int? limit = null, int? offset = null) + { + return _apiClient.GetSearchResults(id, limit, offset); + } + + public Task DeleteSearch(int id) + { + return _apiClient.DeleteSearch(id); + } + + public Task> GetSearchPlugins() + { + return _apiClient.GetSearchPlugins(); + } + + public Task InstallSearchPlugins(params string[] sources) + { + return _apiClient.InstallSearchPlugins(sources); + } + + public Task UninstallSearchPlugins(params string[] names) + { + return _apiClient.UninstallSearchPlugins(names); + } + + public Task EnableSearchPlugins(params string[] names) + { + return _apiClient.EnableSearchPlugins(names); + } + + public Task DisableSearchPlugins(params string[] names) + { + return _apiClient.DisableSearchPlugins(names); + } + + public Task UpdateSearchPlugins() + { + return _apiClient.UpdateSearchPlugins(); + } } } \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SearchCategory.cs b/Lantean.QBitTorrentClient/Models/SearchCategory.cs new file mode 100644 index 0000000..f5344c4 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SearchCategory.cs @@ -0,0 +1,20 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record SearchCategory + { + [JsonConstructor] + public SearchCategory(string id, string name) + { + Id = id; + Name = name; + } + + [JsonPropertyName("id")] + public string Id { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SearchPlugin.cs b/Lantean.QBitTorrentClient/Models/SearchPlugin.cs new file mode 100644 index 0000000..a01eb72 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SearchPlugin.cs @@ -0,0 +1,42 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record SearchPlugin + { + [JsonConstructor] + public SearchPlugin( + bool enabled, + string fullName, + string name, + IReadOnlyList supportedCategories, + string url, + string version) + { + Enabled = enabled; + FullName = fullName; + Name = name; + SupportedCategories = supportedCategories; + Url = url; + Version = version; + } + + [JsonPropertyName("enabled")] + public bool Enabled { get; set; } + + [JsonPropertyName("fullName")] + public string FullName { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("supportedCategories")] + public IReadOnlyList SupportedCategories { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + + [JsonPropertyName("version")] + public string Version { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SearchResult.cs b/Lantean.QBitTorrentClient/Models/SearchResult.cs new file mode 100644 index 0000000..ce8acc7 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SearchResult.cs @@ -0,0 +1,47 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record SearchResult + { + [JsonConstructor] + public SearchResult( + string descriptionLink, + string fileName, + long fileSize, + string fileUrl, + int leechers, + int seeders, + string siteUrl) + { + DescriptionLink = descriptionLink; + FileName = fileName; + FileSize = fileSize; + FileUrl = fileUrl; + Leechers = leechers; + Seeders = seeders; + SiteUrl = siteUrl; + } + + [JsonPropertyName("descrLink")] + public string DescriptionLink { get; set; } + + [JsonPropertyName("fileName")] + public string FileName { get; set; } + + [JsonPropertyName("fileSize")] + public long FileSize { get; set; } + + [JsonPropertyName("fileUrl")] + public string FileUrl { get; set; } + + [JsonPropertyName("nbLeechers")] + public int Leechers { get; set; } + + [JsonPropertyName("nbSeeders")] + public int Seeders { get; set; } + + [JsonPropertyName("siteUrl")] + public string SiteUrl { get; set; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SearchResults.cs b/Lantean.QBitTorrentClient/Models/SearchResults.cs new file mode 100644 index 0000000..7bebfbe --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SearchResults.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record SearchResults + { + [JsonConstructor] + public SearchResults(IReadOnlyList results, string status, int total) + { + Results = results; + Status = status; + Total = total; + } + + [JsonPropertyName("results")] + public IReadOnlyList Results { get; } + + [JsonPropertyName("status")] + public string Status { get; } + + [JsonPropertyName("total")] + public int Total { get; } + } +} \ No newline at end of file diff --git a/Lantean.QBitTorrentClient/Models/SearchStatus.cs b/Lantean.QBitTorrentClient/Models/SearchStatus.cs new file mode 100644 index 0000000..1e85d70 --- /dev/null +++ b/Lantean.QBitTorrentClient/Models/SearchStatus.cs @@ -0,0 +1,24 @@ +using System.Text.Json.Serialization; + +namespace Lantean.QBitTorrentClient.Models +{ + public record SearchStatus + { + [JsonConstructor] + public SearchStatus(int id, string status, int total) + { + Id = id; + Status = status; + Total = total; + } + + [JsonPropertyName("id")] + public int Id { get; } + + [JsonPropertyName("status")] + public string Status { get; } + + [JsonPropertyName("total")] + public int Total { get; } + } +} \ No newline at end of file