Upgrade to MudBlazor 7 and add log pages

This commit is contained in:
ahjephson
2024-06-28 18:05:02 +01:00
parent e9e41950f7
commit 5daa68fc7b
43 changed files with 584 additions and 208 deletions

View File

@@ -3,15 +3,13 @@
<MudGrid> <MudGrid>
<MudItem xs="12"> <MudItem xs="12">
<MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent" MaximumFileCount="50" > <MudFileUpload T="IReadOnlyList<IBrowserFile>" FilesChanged="UploadFiles" Accept=".torrent" MaximumFileCount="50" >
<ButtonTemplate> <ActivatorContent>
<MudButton HtmlTag="label" <MudButton Variant="Variant.Filled"
Variant="Variant.Filled"
Color="Color.Primary" Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload" StartIcon="@Icons.Material.Filled.CloudUpload">
for="@context.Id">
Choose files Choose files
</MudButton> </MudButton>
</ButtonTemplate> </ActivatorContent>
</MudFileUpload> </MudFileUpload>
</MudItem> </MudItem>
</MudGrid> </MudGrid>

View File

@@ -2,7 +2,7 @@
<DialogContent> <DialogContent>
<MudGrid> <MudGrid>
<MudItem xs="12"> <MudItem xs="12">
<MudList Clickable="true"> <MudList T="string">
<MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddCategory">Add</MudListItem> <MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddCategory">Add</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveCategory">Remove</MudListItem> <MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveCategory">Remove</MudListItem>
<MudDivider /> <MudDivider />

View File

@@ -2,7 +2,7 @@
<DialogContent> <DialogContent>
<MudGrid> <MudGrid>
<MudItem xs="12"> <MudItem xs="12">
<MudList Clickable="true"> <MudList T="string">
<MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddTag">Add</MudListItem> <MudListItem Icon="@Icons.Material.Filled.Add" IconColor="Color.Info" OnClick="AddTag">Add</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveAllTags">Remove All</MudListItem> <MudListItem Icon="@Icons.Material.Filled.Remove" IconColor="Color.Error" OnClick="RemoveAllTags">Remove All</MudListItem>
<MudDivider /> <MudDivider />

View File

@@ -1,10 +1,11 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using MudBlazor; using MudBlazor;
using System.Numerics;
namespace Lantean.QBTMudBlade.Components.Dialogs namespace Lantean.QBTMudBlade.Components.Dialogs
{ {
public partial class SliderFieldDialog<T> public partial class SliderFieldDialog<T> where T : struct, INumber<T>
{ {
[CascadingParameter] [CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!; public MudDialogInstance MudDialog { get; set; } = default!;
@@ -13,13 +14,13 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
public string Label { get; set; } = default!; public string Label { get; set; } = default!;
[Parameter] [Parameter]
public T? Value { get; set; } public T Value { get; set; }
[Parameter] [Parameter]
public T? Min { get; set; } public T Min { get; set; } = T.Zero;
[Parameter] [Parameter]
public T? Max { get; set; } public T Max { get; set; } = T.One;
protected void Cancel(MouseEventArgs args) protected void Cancel(MouseEventArgs args)
{ {

View File

@@ -40,7 +40,7 @@
<MudTh Class="@className" Style="@(GetColumnStyle(column))"> <MudTh Class="@className" Style="@(GetColumnStyle(column))">
@if (column.SortSelector is not null) @if (column.SortSelector is not null)
{ {
<SortLabel SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@columnHeader</SortLabel> <SortLabel Class="column-header" SortDirectionChanged="@(c => SetSort(column.Id, c))" SortDirection="@(column.Id == _sortColumn ? _sortDirection : SortDirection.None)">@columnHeader</SortLabel>
} }
else else
{ {

View File

@@ -190,6 +190,10 @@ namespace Lantean.QBTMudBlade.Components
protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs) protected async Task OnRowClickInternal(TableRowClickEventArgs<T> eventArgs)
{ {
if (eventArgs.Item is null)
{
return;
}
if (MultiSelection) if (MultiSelection)
{ {
if (eventArgs.MouseEventArgs.CtrlKey) if (eventArgs.MouseEventArgs.CtrlKey)
@@ -236,7 +240,7 @@ namespace Lantean.QBTMudBlade.Components
//EqualityComparer<T>.Default.Equals(item, SelectedItem) || //EqualityComparer<T>.Default.Equals(item, SelectedItem) ||
if (SelectedItems.Contains(item)) if (SelectedItems.Contains(item))
{ {
style += " background-color: var(--mud-palette-grey-dark); color: var(--mud-palette-grey-light) !important;"; style += " background-color: var(--mud-palette-gray-dark); color: var(--mud-palette-gray-light) !important;";
} }
return style; return style;
} }

View File

@@ -17,8 +17,12 @@ namespace Lantean.QBTMudBlade.Components
[Parameter] [Parameter]
public bool Disabled { get; set; } public bool Disabled { get; set; }
[Inject]
public ILogger<EnhancedErrorBoundary> Logger { get; set; } = default!;
protected override Task OnErrorAsync(Exception exception) protected override Task OnErrorAsync(Exception exception)
{ {
Logger.LogError(exception, exception.Message);
_exceptions.Add(exception); _exceptions.Add(exception);
if (Disabled) if (Disabled)

View File

@@ -1,4 +1,4 @@
<MudList Clickable="true"> <MudList T="string">
<MudListItem OnClick="ClearErrors">Clear Errors</MudListItem> <MudListItem OnClick="ClearErrors">Clear Errors</MudListItem>
<MudListItem OnClick="ClearErrorsAndResumeAsync">Clear Errors and Resume</MudListItem> <MudListItem OnClick="ClearErrorsAndResumeAsync">Clear Errors and Resume</MudListItem>
<MudDivider /> <MudDivider />

View File

@@ -1,20 +1,20 @@
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFile" Title="Rename" /> <MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFile" title="Rename" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" /> <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download"> <MudMenu Icon="@Icons.Material.Outlined.FileDownloadOff" Label="Do Not Download" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Do Not Download">
<MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability" OnTouch="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem> <MudMenuItem OnClick="DoNotDownloadLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability" OnTouch="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem> <MudMenuItem OnClick="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem> <MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu> </MudMenu>
<MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download"> <MudMenu Icon="@Icons.Material.Outlined.FileDownload" Label="Normal Priority" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" title="Download">
<MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability" OnTouch="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem> <MudMenuItem OnClick="NormalPriorityLessThan100PercentAvailability">Less Than 100% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability" OnTouch="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem> <MudMenuItem OnClick="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem> <MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu> </MudMenu>
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" Title="Filter" /> <MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" title="Filter" />
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" Title="Remove Filter" /> <MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" title="Remove Filter" />
<MudSpacer /> <MudSpacer />
<MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField> <MudTextField T="string" Value="SearchText" ValueChanged="SearchTextChanged" Immediate="true" DebounceInterval="500" Placeholder="Filter file list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar> </MudToolBar>

View File

@@ -89,7 +89,7 @@ namespace Lantean.QBTMudBlade.Components
var result = await DialogService.ShowAsync<FilterOptionsDialog<ContentItem>>("Filters", parameters, DialogHelper.FormDialogOptions); var result = await DialogService.ShowAsync<FilterOptionsDialog<ContentItem>>("Filters", parameters, DialogHelper.FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }

View File

@@ -1,4 +1,4 @@
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft"> <MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft">
<MudMenuItem Icon="@Icons.Material.Filled.PieChart" Href="/statistics">Statistics</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.PieChart" Href="/statistics">Statistics</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Search" Href="/search">Search</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.Search" Href="/search">Search</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.RssFeed" Href="/rss">RSS</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.RssFeed" Href="/rss">RSS</MudMenuItem>
@@ -6,8 +6,8 @@
<MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" Href="/blocks">Blocked IPs</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.DisabledByDefault" Href="/blocks">Blocked IPs</MudMenuItem>
<MudDivider /> <MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Settings" Href="/settings">Settings</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.Settings" Href="/settings">Settings</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Undo" OnClick="ResetWebUI" OnTouch="ResetWebUI">Reset Web UI</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.Undo" OnClick="ResetWebUI">Reset Web UI</MudMenuItem>
<MudDivider /> <MudDivider />
<MudMenuItem Icon="@Icons.Material.Filled.Logout" OnClick="Logout" OnTouch="Logout">Logout</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.Logout" OnClick="Logout">Logout</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" OnClick="Exit" OnTouch="Exit">Exit qBittorrent</MudMenuItem> <MudMenuItem Icon="@Icons.Material.Filled.ExitToApp" OnClick="Exit">Exit qBittorrent</MudMenuItem>
</MudMenu> </MudMenu>

View File

@@ -289,7 +289,7 @@
</MudItem> </MudItem>
<MudItem xs="12"> <MudItem xs="12">
<MudText>Supported parameters (case sensitive):</MudText> <MudText>Supported parameters (case sensitive):</MudText>
<MudList> <MudList T="string" ReadOnly="true">
<MudListItem>%N: Torrent name</MudListItem> <MudListItem>%N: Torrent name</MudListItem>
<MudListItem>%L: Category</MudListItem> <MudListItem>%L: Category</MudListItem>
<MudListItem>%G: Tags (separated by comma)</MudListItem> <MudListItem>%G: Tags (separated by comma)</MudListItem>

View File

@@ -20,7 +20,7 @@ namespace Lantean.QBTMudBlade.Components
[Parameter] [Parameter]
public bool Active { get; set; } public bool Active { get; set; }
[CascadingParameter] [CascadingParameter(Name = "RefreshInterval")]
public int RefreshInterval { get; set; } public int RefreshInterval { get; set; }
[CascadingParameter] [CascadingParameter]

View File

@@ -51,9 +51,9 @@ namespace Lantean.QBTMudBlade.Components
} }
else else
{ {
downloadingColor = Theme.Palette.Success.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA); downloadingColor = Theme.PaletteLight.Success.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
haveColor = Theme.Palette.Info.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA); haveColor = Theme.PaletteLight.Info.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
borderColor = Theme.Palette.Black.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA); borderColor = Theme.PaletteLight.Black.ToString(MudBlazor.Utilities.MudColorOutputFormats.RGBA);
} }
await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray(), downloadingColor, haveColor, borderColor); await JSRuntime.RenderPiecesBar("progress", Hash, Pieces.Select(s => (int)s).ToArray(), downloadingColor, haveColor, borderColor);
} }

View File

@@ -1,6 +1,6 @@
@if (RenderType == RenderType.Toolbar) @if (RenderType == RenderType.Toolbar)
{ {
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true"> <MudToolBar Dense="true" Gutters="false" WrapContent="true">
@ToolbarContent @ToolbarContent
</MudToolBar> </MudToolBar>
} }
@@ -10,7 +10,7 @@ else if (RenderType == RenderType.ToolbarContents)
} }
else if (RenderType == RenderType.MixedToolbar) else if (RenderType == RenderType.MixedToolbar)
{ {
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true"> <MudToolBar Dense="true" Gutters="false" WrapContent="true">
@MixedToolbarContent @MixedToolbarContent
</MudToolBar> </MudToolBar>
} }
@@ -27,7 +27,7 @@ else if (RenderType == RenderType.InitialIconsOnly)
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudIconButton Title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" /> <MudIconButton title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
} }
@Menu(Actions.Skip(5)) @Menu(Actions.Skip(5))
@@ -37,7 +37,7 @@ else if (RenderType == RenderType.Children)
var parent = Actions.FirstOrDefault(a => a.Text == ParentAction?.Text); var parent = Actions.FirstOrDefault(a => a.Text == ParentAction?.Text);
if (parent is not null) if (parent is not null)
{ {
<MudList Clickable="true"> <MudList T="string">
@foreach (var action in parent.Children) @foreach (var action in parent.Children)
{ {
@if (action.SeparatorBefore) @if (action.SeparatorBefore)
@@ -77,7 +77,7 @@ else
} }
else else
{ {
<MudIconButton Title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" /> <MudIconButton title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
} }
} }
else else
@@ -115,7 +115,7 @@ else
} }
else else
{ {
<MudIconButton Title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" /> <MudIconButton title="@action.Text" Icon="@action.Icon" Color="action.Color" OnClick="action.Callback" Disabled="Disabled" />
} }
} }
else else
@@ -141,7 +141,7 @@ else
<MudDivider /> <MudDivider />
} }
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" OnTouch="action.Callback" Disabled="Disabled">@action.Text</MudMenuItem> <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">@action.Text</MudMenuItem>
}; };
} }
@@ -149,7 +149,7 @@ else
{ {
return __builder => return __builder =>
{ {
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" Label="Actions" EndIcon="@Icons.Material.Filled.ArrowDropDown" @ref="ActionsMenu" Disabled="@(!Hashes.Any())"> <MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" Label="Actions" EndIcon="@Icons.Material.Filled.ArrowDropDown" @ref="ActionsMenu" Disabled="@(!Hashes.Any())" ActivationEvent="MouseEvent.LeftClick">
@foreach (var action in actions) @foreach (var action in actions)
{ {
@if (action.SeparatorBefore) @if (action.SeparatorBefore)
@@ -159,14 +159,14 @@ else
if (!action.Children.Any()) if (!action.Children.Any())
{ {
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" OnTouch="action.Callback" Disabled="Disabled"> <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="action.Callback" Disabled="Disabled">
@action.Text @action.Text
</MudMenuItem> </MudMenuItem>
} }
else else
{ {
<MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnTouch="@(t => SubMenuTouch(action))" OnClick="@(t => SubMenuTouch(action))"> <MudMenuItem Icon="@action.Icon" IconColor="action.Color" OnClick="@(t => SubMenuTouch(action))">
<MudMenu Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" DisableElevation="true" DisableRipple="true" Class="sub-menu"> <MudMenu Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" Ripple="false" Class="sub-menu">
<ActivatorContent> <ActivatorContent>
@action.Text @action.Text
</ActivatorContent> </ActivatorContent>

View File

@@ -66,7 +66,7 @@ namespace Lantean.QBTMudBlade.Components
{ {
new TorrentAction("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)), new TorrentAction("start", "Start", Icons.Material.Filled.PlayArrow, Color.Success, CreateCallback(Resume)),
new TorrentAction("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)), new TorrentAction("pause", "Pause", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(Pause)),
new TorrentAction("forceStart", "Force start", Icons.Material.Filled.Pause, Color.Warning, CreateCallback(ForceStart)), new TorrentAction("forceStart", "Force start", Icons.Material.Filled.Forward, Color.Warning, CreateCallback(ForceStart)),
new TorrentAction("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true), new TorrentAction("delete", "Remove", Icons.Material.Filled.Delete, Color.Error, CreateCallback(Remove), separatorBefore: true),
new TorrentAction("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true), new TorrentAction("setLocation", "Set location", Icons.Material.Filled.MyLocation, Color.Info, CreateCallback(SetLocation), separatorBefore: true),
new TorrentAction("rename", "Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)), new TorrentAction("rename", "Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, CreateCallback(Rename)),
@@ -248,7 +248,10 @@ namespace Lantean.QBTMudBlade.Components
protected async Task Copy(Func<Torrent, object?> selector) protected async Task Copy(Func<Torrent, object?> selector)
{ {
await Copy(string.Join(Environment.NewLine, GetTorrents().Select(selector))); await Copy(string.Join(Environment.NewLine, GetTorrents().Select(selector)));
ActionsMenu?.CloseMenu(); if (ActionsMenu is not null)
{
await ActionsMenu.CloseMenuAsync();
}
} }
protected async Task Export() protected async Task Export()
@@ -487,7 +490,6 @@ namespace Lantean.QBTMudBlade.Components
} }
else else
{ {
if (actionState.Show is null || actionState.Show.Value) if (actionState.Show is null || actionState.Show.Value)
{ {
var act = action with { }; var act = action with { };
@@ -499,6 +501,8 @@ namespace Lantean.QBTMudBlade.Components
{ {
act.IsChecked = actionState.IsChecked.Value; act.IsChecked = actionState.IsChecked.Value;
} }
yield return act;
} }
} }
} }

View File

@@ -3,7 +3,7 @@
return; return;
} }
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true"> <MudToolBar Dense="true" Gutters="false" WrapContent="true">
@{ @{
var (icon, color) = DisplayHelpers.GetStateIcon(Torrent.State); var (icon, color) = DisplayHelpers.GetStateIcon(Torrent.State);
} }

View File

@@ -17,7 +17,7 @@ namespace Lantean.QBTMudBlade.Components
[Parameter] [Parameter]
public bool Active { get; set; } public bool Active { get; set; }
[CascadingParameter] [CascadingParameter(Name = "RefreshInterval")]
public int RefreshInterval { get; set; } public int RefreshInterval { get; set; }
[Inject] [Inject]

View File

@@ -17,7 +17,7 @@ namespace Lantean.QBTMudBlade.Components
[Parameter, EditorRequired] [Parameter, EditorRequired]
public string? Hash { get; set; } public string? Hash { get; set; }
[CascadingParameter] [CascadingParameter(Name = "RefreshInterval")]
public int RefreshInterval { get; set; } public int RefreshInterval { get; set; }
[Inject] [Inject]

View File

@@ -9,11 +9,11 @@ namespace Lantean.QBTMudBlade
{ {
public static class DialogHelper public static class DialogHelper
{ {
public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, ClassBackground = "background-blur", FullWidth = true }; public static readonly DialogOptions FormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, BackgroundClass = "background-blur", FullWidth = true };
public static readonly DialogOptions NonBlurFormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true }; public static readonly DialogOptions NonBlurFormDialogOptions = new() { CloseButton = true, MaxWidth = MaxWidth.Medium, FullWidth = true };
public static readonly DialogOptions ConfirmDialogOptions = new() { ClassBackground = "background-blur", MaxWidth = MaxWidth.Small, FullWidth = true }; public static readonly DialogOptions ConfirmDialogOptions = new() { BackgroundClass = "background-blur", MaxWidth = MaxWidth.Small, FullWidth = true };
public static readonly DialogOptions NonBlurConfirmDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true }; public static readonly DialogOptions NonBlurConfirmDialogOptions = new() { MaxWidth = MaxWidth.Small, FullWidth = true };
@@ -23,7 +23,7 @@ namespace Lantean.QBTMudBlade
{ {
var result = await dialogService.ShowAsync<AddTorrentFileDialog>("Upload local torrent", FormDialogOptions); var result = await dialogService.ShowAsync<AddTorrentFileDialog>("Upload local torrent", FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -69,7 +69,7 @@ namespace Lantean.QBTMudBlade
{ {
var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", FormDialogOptions); var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -104,13 +104,13 @@ namespace Lantean.QBTMudBlade
}; };
var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions); var reference = await dialogService.ShowAsync<DeleteDialog>($"Remove torrent{(hashes.Length == 1 ? "" : "s")}?", parameters, ConfirmDialogOptions);
var result = await reference.Result; var dialogResult = await reference.Result;
if (result.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
await apiClient.DeleteTorrents(hashes, (bool)result.Data); await apiClient.DeleteTorrents(hashes, (bool)dialogResult.Data);
} }
public static async Task InvokeRenameFilesDialog(this IDialogService dialogService, IApiClient apiClient, string hash) public static async Task InvokeRenameFilesDialog(this IDialogService dialogService, IApiClient apiClient, string hash)
@@ -121,13 +121,13 @@ namespace Lantean.QBTMudBlade
public static async Task<string?> ShowAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient) public static async Task<string?> ShowAddCategoryDialog(this IDialogService dialogService, IApiClient apiClient)
{ {
var reference = await dialogService.ShowAsync<AddCategoryDialog>("New Category", NonBlurFormDialogOptions); var reference = await dialogService.ShowAsync<AddCategoryDialog>("New Category", NonBlurFormDialogOptions);
var result = await reference.Result; var dialogResult = await reference.Result;
if (result.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return null; return null;
} }
var category = (Category)result.Data; var category = (Category)dialogResult.Data;
await apiClient.AddCategory(category.Name, category.SavePath); await apiClient.AddCategory(category.Name, category.SavePath);
@@ -136,15 +136,15 @@ namespace Lantean.QBTMudBlade
public static async Task<HashSet<string>?> ShowAddTagsDialog(this IDialogService dialogService, IApiClient apiClient) public static async Task<HashSet<string>?> ShowAddTagsDialog(this IDialogService dialogService, IApiClient apiClient)
{ {
var dialogReference = await dialogService.ShowAsync<AddTagDialog>("Add Tags", NonBlurFormDialogOptions); var reference = await dialogService.ShowAsync<AddTagDialog>("Add Tags", NonBlurFormDialogOptions);
var result = await dialogReference.Result; var dialogResult = await reference.Result;
if (result.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return null; return null;
} }
var tags = (HashSet<string>)result.Data; var tags = (HashSet<string>)dialogResult.Data;
return tags; return tags;
} }
@@ -158,7 +158,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions); var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
return !dialogResult.Canceled; return dialogResult is not null && !dialogResult.Canceled;
} }
public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func<Task> onSuccess) public static async Task ShowConfirmDialog(this IDialogService dialogService, string title, string content, Func<Task> onSuccess)
@@ -170,7 +170,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions); var result = await dialogService.ShowAsync<ConfirmDialog>(title, parameters, ConfirmDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -198,7 +198,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<SingleFieldDialog<T>>(title, parameters, FormDialogOptions); var result = await dialogService.ShowAsync<SingleFieldDialog<T>>(title, parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -217,7 +217,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Download Rate", parameters, FormDialogOptions); var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Download Rate", parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -236,7 +236,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions); var result = await dialogService.ShowAsync<SliderFieldDialog<long>>("Upload Rate", parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -255,7 +255,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<SliderFieldDialog<float>>("Upload Rate", parameters, FormDialogOptions); var result = await dialogService.ShowAsync<SliderFieldDialog<float>>("Upload Rate", parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return; return;
} }
@@ -273,7 +273,7 @@ namespace Lantean.QBTMudBlade
var result = await dialogService.ShowAsync<FilterOptionsDialog<T>>("Filters", parameters, FormDialogOptions); var result = await dialogService.ShowAsync<FilterOptionsDialog<T>>("Filters", parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return null; return null;
} }
@@ -291,13 +291,13 @@ namespace Lantean.QBTMudBlade
}; };
var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions); var reference = await dialogService.ShowAsync<ColumnOptionsDialog<T>>("Column Options", parameters, FormDialogOptions);
var result = await reference.Result; var dialogResult = await reference.Result;
if (result.Canceled) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {
return default; return default;
} }
return ((HashSet<string>, Dictionary<string, int?>))result.Data; return ((HashSet<string>, Dictionary<string, int?>))dialogResult.Data;
} }
public static async Task InvokeRssRulesDialog(this IDialogService dialogService) public static async Task InvokeRssRulesDialog(this IDialogService dialogService)

View File

@@ -14,7 +14,7 @@
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.6" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.6" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="MudBlazor" Version="6.20.0" /> <PackageReference Include="MudBlazor" Version="7.0.0-rc.2" />
<PackageReference Include="MudBlazor.ThemeManager" Version="1.1.0" /> <PackageReference Include="MudBlazor.ThemeManager" Version="1.1.0" />
</ItemGroup> </ItemGroup>

View File

@@ -1,7 +1,7 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@layout LoggedInLayout @layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true"> <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" /> <TorrentsListNav Torrents="Torrents" SelectedTorrent="@SelectedTorrent" SortDirection="SortDirection" SortColumn="@SortColumn" />
</MudDrawer> </MudDrawer>
<MudMainContent> <MudMainContent>

View File

@@ -1,7 +1,7 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@layout LoggedInLayout @layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true"> <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" /> <FiltersNav CategoryChanged="CategoryChanged" StatusChanged="StatusChanged" TagChanged="TagChanged" TrackerChanged="TrackerChanged" />
</MudDrawer> </MudDrawer>
<MudMainContent> <MudMainContent>

View File

@@ -5,6 +5,7 @@
<MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" /> <MudThemeProvider @ref="MudThemeProvider" @bind-IsDarkMode="IsDarkMode" Theme="Theme" />
<MudDialogProvider /> <MudDialogProvider />
<MudSnackbarProvider /> <MudSnackbarProvider />
<MudPopoverProvider />
<PageTitle>qBittorrent Web UI</PageTitle> <PageTitle>qBittorrent Web UI</PageTitle>

View File

@@ -1,7 +1,7 @@
@inherits LayoutComponentBase @inherits LayoutComponentBase
@layout LoggedInLayout @layout LoggedInLayout
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" DisableOverlay="true"> <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<MudNavMenu> <MudNavMenu>
<MudNavLink Icon="@(Icons.Material.Outlined.NavigateBefore)" OnClick="NavigateBack">Back</MudNavLink> <MudNavLink Icon="@(Icons.Material.Outlined.NavigateBefore)" OnClick="NavigateBack">Back</MudNavLink>
<MudDivider /> <MudDivider />

View File

@@ -0,0 +1,16 @@
namespace Lantean.QBTMudBlade.Models
{
public class LogForm
{
public bool Normal => SelectedTypes.Contains("Normal");
public bool Info => SelectedTypes.Contains("Info");
public bool Warning => SelectedTypes.Contains("Warning");
public bool Critical => SelectedTypes.Contains("Critical");
public int? LastKnownId { get; set; }
public IEnumerable<string> SelectedTypes { get; set; } = new HashSet<string>();
public string? Criteria { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
namespace Lantean.QBTMudBlade.Models
{
public class LoginForm
{
[Required]
[NotNull]
public string? Username { get; set; }
[Required]
[NotNull]
public string? Password { get; set; }
}
}

View File

@@ -0,0 +1,11 @@
namespace Lantean.QBTMudBlade.Models
{
public class SearchForm
{
public string? SearchText { get; set; }
public string SelectedPlugin { get; set; } = "all";
public string SelectedCategory { get; set; } = "all";
}
}

View File

@@ -1,16 +1,59 @@
@page "/blocks" @page "/blocks"
@layout OtherLayout @layout OtherLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Blocked IPs</MudText> <MudText Class="pl-5 no-wrap">Blocked IPs</MudText>
</MudToolBar> </MudToolBar>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents"> <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<p>Coming soon.</p> <MudCardContent>
</MudContainer> <EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="10">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" ShrinkLabel Variant="Variant.Outlined" />
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<MudTable Items="Results"
T="Lantean.QBitTorrentClient.Models.PeerLog"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false"
Class="search-list"
RowClassFunc="RowClass">
<HeaderContent>
<MudTh>Id</MudTh>
<MudTh>Message</MudTh>
<MudTh>Timestamp</MudTh>
<MudTh>Status</MudTh>
<MudTh>Reason</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Id">@context.Id</MudTd>
<MudTd DataLabel="IP">@context.IPAddress</MudTd>
<MudTd DataLabel="Timestamp">@DisplayHelpers.DateTime(context.Timestamp)</MudTd>
<MudTd DataLabel="Status">@(context.Blocked ? "Blocked" : "Banned")</MudTd>
<MudTd DataLabel="Reason">@context.Reason</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -1,12 +1,21 @@
using Lantean.QBitTorrentClient; using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor; using MudBlazor;
using System.Net;
namespace Lantean.QBTMudBlade.Pages namespace Lantean.QBTMudBlade.Pages
{ {
public partial class Blocks public partial class Blocks : IAsyncDisposable
{ {
private readonly bool _refreshEnabled = true;
private const string _selectedTypesStorageKey = "Blocks.SelectedTypes";
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
[Inject] [Inject]
protected IApiClient ApiClient { get; set; } = default!; protected IApiClient ApiClient { get; set; } = default!;
@@ -16,24 +25,129 @@ namespace Lantean.QBTMudBlade.Pages
[Inject] [Inject]
protected NavigationManager NavigationManager { get; set; } = default!; protected NavigationManager NavigationManager { get; set; } = default!;
[CascadingParameter] [Inject]
public MainData? MainData { get; set; } protected ILocalStorageService LocalStorage { get; set; } = default!;
[CascadingParameter(Name = "DrawerOpen")] [CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; } public bool DrawerOpen { get; set; }
[Parameter] protected LogForm Model { get; set; } = new LogForm();
public string? Hash { get; set; }
protected int ActiveTab { get; set; } = 0; protected List<QBitTorrentClient.Models.PeerLog>? Results { get; private set; }
protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; protected MudSelect<string>? CategoryMudSelect { get; set; }
protected ServerState? ServerState => MainData?.ServerState; protected override async Task OnInitializedAsync()
{
var selectedTypes = await LocalStorage.GetItemAsync<IEnumerable<string>>(_selectedTypesStorageKey);
if (selectedTypes is not null)
{
Model.SelectedTypes = selectedTypes;
}
else
{
Model.SelectedTypes = ["Normal"];
}
await DoSearch();
}
protected void NavigateBack() protected void NavigateBack()
{ {
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
protected async Task SelectedValuesChanged(IEnumerable<string> values)
{
Model.SelectedTypes = values;
await LocalStorage.SetItemAsync(_selectedTypesStorageKey, Model.SelectedTypes);
}
protected static string GenerateSelectedText(List<string> values)
{
if (values.Count == 4)
{
return "All";
}
return $"{values.Count} selected";
}
protected Task Submit(EditContext editContext)
{
return DoSearch();
}
private async Task DoSearch()
{
var results = await ApiClient.GetPeerLog(Model.LastKnownId);
if (results.Count > 0)
{
Results ??= [];
Results.AddRange(results);
Model.LastKnownId = results[^1].Id;
}
}
protected static string RowClass(QBitTorrentClient.Models.PeerLog log, int index)
{
return $"log-{(log.Blocked ? "critical" : "normal")}";
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.CompletedTask;
}
_disposedValue = true;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_refreshEnabled)
{
return;
}
if (!firstRender)
{
return;
}
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1500)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
try
{
await DoSearch();
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden || exception.StatusCode == HttpStatusCode.NotFound)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
await InvokeAsync(StateHasChanged);
}
}
}
} }
} }

View File

@@ -1,10 +1,10 @@
@page "/details/{hash}" @page "/details/{hash}"
@layout DetailsLayout @layout DetailsLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
@if (Hash is not null) @if (Hash is not null)
@@ -17,7 +17,7 @@
@if (ShowTabs) @if (ShowTabs)
{ {
<CascadingValue Value="RefreshInterval"> <CascadingValue Value="RefreshInterval" Name="RefreshInterval">
<MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true"> <MudTabs Elevation="2" ApplyEffectsToContainer="true" @bind-ActivePanelIndex="ActiveTab" KeepPanelsAlive="true" Border="true">
<MudTabPanel Text="General"> <MudTabPanel Text="General">
<GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" /> <GeneralTab Hash="@Hash" Active="@(ActiveTab == 0)" />

View File

@@ -1,16 +1,65 @@
@page "/log" @page "/log"
@layout OtherLayout @layout OtherLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Execution Log</MudText> <MudText Class="pl-5 no-wrap">Execution Log</MudText>
</MudToolBar> </MudToolBar>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents"> <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<p>Coming soon.</p> <MudCardContent>
</MudContainer> <EditForm Model="Model" OnSubmit="Submit">
<MudGrid>
<MudItem md="8">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.Criteria" ShrinkLabel Variant="Variant.Outlined" />
</MudItem>
<MudItem md="2">
<MudSelect @ref="CategoryMudSelect" T="string" Label="Categories" SelectedValues="Model.SelectedTypes" SelectedValuesChanged="SelectedValuesChanged" ShrinkLabel Variant="Variant.Outlined" MultiSelection="true" MultiSelectionTextFunc="GenerateSelectedText" SelectAll="true">
<MudSelectItem Value="@("Normal")">Normal</MudSelectItem>
<MudSelectItem Value="@("Info")">Info</MudSelectItem>
<MudSelectItem Value="@("Warning")">Warning</MudSelectItem>
<MudSelectItem Value="@("Critical")">Critical</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem md="2">
<MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">Filter</MudButton>
</MudItem>
</MudGrid>
</EditForm>
</MudCardContent>
</MudCard>
<MudTable Items="Results"
T="Lantean.QBitTorrentClient.Models.Log"
Hover="true"
FixedHeader="true"
HeaderClass="table-head-bordered"
Dense="true"
Breakpoint="Breakpoint.None"
Bordered="true"
Square="true"
LoadingProgressColor="Color.Info"
HorizontalScrollbar="true"
Virtualize="true"
AllowUnsorted="false"
SelectOnRowClick="false"
Class="search-list"
RowClassFunc="RowClass">
<HeaderContent>
<MudTh>Id</MudTh>
<MudTh>Message</MudTh>
<MudTh>Timestamp</MudTh>
<MudTh>Log Type</MudTh>
</HeaderContent>
<RowTemplate>
<MudTd DataLabel="Od">@context.Id</MudTd>
<MudTd DataLabel="Message">@context.Message</MudTd>
<MudTd DataLabel="Timestamp">@DisplayHelpers.DateTime(context.Timestamp)</MudTd>
<MudTd DataLabel="Log Type">@context.Type</MudTd>
</RowTemplate>
</MudTable>

View File

@@ -1,12 +1,21 @@
using Lantean.QBitTorrentClient; using Blazored.LocalStorage;
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor; using MudBlazor;
using System.Net;
namespace Lantean.QBTMudBlade.Pages namespace Lantean.QBTMudBlade.Pages
{ {
public partial class Log public partial class Log : IAsyncDisposable
{ {
private readonly bool _refreshEnabled = true;
private const string _selectedTypesStorageKey = "Log.SelectedTypes";
private readonly CancellationTokenSource _timerCancellationToken = new();
private bool _disposedValue;
[Inject] [Inject]
protected IApiClient ApiClient { get; set; } = default!; protected IApiClient ApiClient { get; set; } = default!;
@@ -16,24 +25,129 @@ namespace Lantean.QBTMudBlade.Pages
[Inject] [Inject]
protected NavigationManager NavigationManager { get; set; } = default!; protected NavigationManager NavigationManager { get; set; } = default!;
[CascadingParameter] [Inject]
public MainData? MainData { get; set; } protected ILocalStorageService LocalStorage { get; set; } = default!;
[CascadingParameter(Name = "DrawerOpen")] [CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; } public bool DrawerOpen { get; set; }
[Parameter] protected LogForm Model { get; set; } = new LogForm();
public string? Hash { get; set; }
protected int ActiveTab { get; set; } = 0; protected List<QBitTorrentClient.Models.Log>? Results { get; private set; }
protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500; protected MudSelect<string>? CategoryMudSelect { get; set; }
protected ServerState? ServerState => MainData?.ServerState; protected override async Task OnInitializedAsync()
{
var selectedTypes = await LocalStorage.GetItemAsync<IEnumerable<string>>(_selectedTypesStorageKey);
if (selectedTypes is not null)
{
Model.SelectedTypes = selectedTypes;
}
else
{
Model.SelectedTypes = ["Normal"];
}
await DoSearch();
}
protected void NavigateBack() protected void NavigateBack()
{ {
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
protected async Task SelectedValuesChanged(IEnumerable<string> values)
{
Model.SelectedTypes = values;
await LocalStorage.SetItemAsync(_selectedTypesStorageKey, Model.SelectedTypes);
}
protected static string GenerateSelectedText(List<string> values)
{
if (values.Count == 4)
{
return "All";
}
return $"{values.Count} selected";
}
protected Task Submit(EditContext editContext)
{
return DoSearch();
}
private async Task DoSearch()
{
var results = await ApiClient.GetLog(Model.Normal, Model.Info, Model.Warning, Model.Critical, Model.LastKnownId);
if (results.Count > 0)
{
Results ??= [];
Results.AddRange(results);
Model.LastKnownId = results[^1].Id;
}
}
protected static string RowClass(QBitTorrentClient.Models.Log log, int index)
{
return $"log-{log.Type.ToString().ToLower()}";
}
public async ValueTask DisposeAsync()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
await DisposeAsync(disposing: true);
GC.SuppressFinalize(this);
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_timerCancellationToken.Cancel();
_timerCancellationToken.Dispose();
await Task.CompletedTask;
}
_disposedValue = true;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!_refreshEnabled)
{
return;
}
if (!firstRender)
{
return;
}
using (var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(1500)))
{
while (!_timerCancellationToken.IsCancellationRequested && await timer.WaitForNextTickAsync())
{
try
{
await DoSearch();
}
catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden || exception.StatusCode == HttpStatusCode.NotFound)
{
_timerCancellationToken.CancelIfNotDisposed();
return;
}
await InvokeAsync(StateHasChanged);
}
}
}
} }
} }

View File

@@ -1,8 +1,7 @@
using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms; using Microsoft.AspNetCore.Components.Forms;
using System.ComponentModel.DataAnnotations;
using System.Diagnostics.CodeAnalysis;
using System.Net; using System.Net;
namespace Lantean.QBTMudBlade.Pages namespace Lantean.QBTMudBlade.Pages
@@ -15,7 +14,7 @@ namespace Lantean.QBTMudBlade.Pages
[Inject] [Inject]
protected NavigationManager NavigationManager { get; set; } = default!; protected NavigationManager NavigationManager { get; set; } = default!;
protected LoginModel Model { get; set; } = new LoginModel(); protected LoginForm Model { get; set; } = new LoginForm();
protected string? ApiError { get; set; } protected string? ApiError { get; set; }
@@ -49,19 +48,8 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG #if DEBUG
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
return DoLogin("admin", "6K3mtPNnQ"); return DoLogin("admin", "STMeVwB22");
} }
#endif #endif
} }
public class LoginModel
{
[Required]
[NotNull]
public string? Username { get; set; }
[Required]
[NotNull]
public string? Password { get; set; }
}
} }

View File

@@ -3,7 +3,7 @@
<NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" /> <NavigationLock ConfirmExternalNavigation="@(UpdatePreferences is not null)" OnBeforeInternalNavigation="ValidateExit" />
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" />

View File

@@ -1,10 +1,10 @@
@page "/rss" @page "/rss"
@layout OtherLayout @layout OtherLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />

View File

@@ -1,49 +1,51 @@
@page "/search" @page "/search"
@layout OtherLayout @layout OtherLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Search</MudText> <MudText Class="pl-5 no-wrap">Search</MudText>
</MudToolBar> </MudToolBar>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4 mt-4"> <MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
<MudCardContent Class="pt-0"> <MudCardContent>
<MudGrid> <EditForm Model="Model" OnValidSubmit="DoSearch">
<MudItem xs="12" md="6"> <MudGrid>
<MudTextField T="string" Label="Search" Value="SearchText" ValueChanged="SearchTextChanged" ShrinkLabel Variant="Variant.Outlined" /> <MudItem xs="12" md="6">
</MudItem> <MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" ShrinkLabel Variant="Variant.Outlined" />
<MudItem md="2"> </MudItem>
<MudSelect T="string" Label="Categories" Value="SelectedCategory" ValueChanged="SelectedCategoryChanged" ShrinkLabel Variant="Variant.Outlined"> <MudItem md="2">
@foreach (var (value, name) in Categories) <MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" ShrinkLabel Variant="Variant.Outlined">
{ @foreach (var (value, name) in Categories)
<MudSelectItem Value="value">@name</MudSelectItem>
if (value == "all")
{ {
<MudDivider /> <MudSelectItem Value="value">@name</MudSelectItem>
if (value == "all")
{
<MudDivider />
}
} }
} </MudSelect>
</MudSelect> </MudItem>
</MudItem> <MudItem md="2">
<MudItem md="2"> <MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" ShrinkLabel Variant="Variant.Outlined">
<MudSelect T="string" Label="Plugins" Value="SelectedPlugin" ValueChanged="SelectedPluginChanged" ShrinkLabel Variant="Variant.Outlined"> <MudSelectItem Value="@("all")">All</MudSelectItem>
<MudSelectItem Value="@("all")">All</MudSelectItem> <MudDivider />
<MudDivider /> @foreach (var (value, name) in Plugins)
@foreach (var (value, name) in Plugins) {
{ <MudSelectItem Value="value">@name</MudSelectItem>
<MudSelectItem Value="value">@name</MudSelectItem> }
} </MudSelect>
</MudSelect> </MudItem>
</MudItem> <MudItem md="2">
<MudItem md="2"> <MudButton ButtonType="ButtonType.Submit" FullWidth="true" Color="Color.Primary" EndIcon="@Icons.Material.Filled.Search" Variant="Variant.Filled" Class="mt-6">@(_searchId is null ? "Search" : "Stop")</MudButton>
<MudButton OnClick="DoSearch" FullWidth="true" Variant="Variant.Outlined">@(_searchId is null ? "Search" : "Stop")</MudButton> </MudItem>
</MudItem>
</MudGrid> </MudGrid>
</EditForm>
</MudCardContent> </MudCardContent>
</MudCard> </MudCard>
@@ -62,7 +64,7 @@
Virtualize="true" Virtualize="true"
AllowUnsorted="false" AllowUnsorted="false"
SelectOnRowClick="false" SelectOnRowClick="false"
Class="details-list"> Class="search-list">
<HeaderContent> <HeaderContent>
<MudTh>Name</MudTh> <MudTh>Name</MudTh>
<MudTh>Size</MudTh> <MudTh>Size</MudTh>

View File

@@ -1,6 +1,7 @@
using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Forms;
using MudBlazor; using MudBlazor;
namespace Lantean.QBTMudBlade.Pages namespace Lantean.QBTMudBlade.Pages
@@ -33,21 +34,11 @@ namespace Lantean.QBTMudBlade.Pages
[Parameter] [Parameter]
public string? Hash { get; set; } public string? Hash { get; set; }
protected int ActiveTab { get; set; } = 0; protected SearchForm Model { get; set; } = new SearchForm();
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<string, string> Plugins => _plugins is null ? [] : _plugins.ToDictionary(a => a.Name, a => a.FullName); protected Dictionary<string, string> Plugins => _plugins is null ? [] : _plugins.ToDictionary(a => a.Name, a => a.FullName);
protected Dictionary<string, string> Categories => GetCategories(SelectedPlugin); protected Dictionary<string, string> Categories => GetCategories(Model.SelectedPlugin);
protected IEnumerable<QBitTorrentClient.Models.SearchResult>? Results => _searchResults?.Results; protected IEnumerable<QBitTorrentClient.Models.SearchResult>? Results => _searchResults?.Results;
@@ -97,21 +88,6 @@ namespace Lantean.QBTMudBlade.Pages
NavigationManager.NavigateTo("/"); 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<string, string> GetCategories(string plugin) private Dictionary<string, string> GetCategories(string plugin)
{ {
if (_plugins is null) if (_plugins is null)
@@ -133,17 +109,17 @@ namespace Lantean.QBTMudBlade.Pages
return pluginItem.SupportedCategories.ToDictionary(a => a.Id, a => a.Name); return pluginItem.SupportedCategories.ToDictionary(a => a.Id, a => a.Name);
} }
protected async Task DoSearch() protected async Task DoSearch(EditContext editContext)
{ {
if (_searchId is null) if (_searchId is null)
{ {
if (string.IsNullOrEmpty(SearchText)) if (string.IsNullOrEmpty(Model.SearchText))
{ {
return; return;
} }
_searchResults = null; _searchResults = null;
_searchId = await ApiClient.StartSearch(SearchText, [SelectedPlugin], SelectedCategory); _searchId = await ApiClient.StartSearch(Model.SearchText, [Model.SelectedPlugin], Model.SelectedCategory);
} }
else else
{ {

View File

@@ -1,10 +1,10 @@
@page "/statistics" @page "/statistics"
@layout OtherLayout @layout OtherLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen) @if (!DrawerOpen)
{ {
<MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" Title="Back to torrent list" /> <MudIconButton Icon="@Icons.Material.Outlined.NavigateBefore" OnClick="NavigateBack" title="Back to torrent list" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
} }
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />

View File

@@ -22,13 +22,14 @@ namespace Lantean.QBTMudBlade.Pages
[CascadingParameter(Name = "DrawerOpen")] [CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; } public bool DrawerOpen { get; set; }
[CascadingParameter(Name = "RefreshInterval")]
public int RefreshInterval { get; set; }
[Parameter] [Parameter]
public string? Hash { get; set; } public string? Hash { get; set; }
protected int ActiveTab { get; set; } = 0; protected int ActiveTab { get; set; } = 0;
protected int RefreshInterval => MainData?.ServerState.RefreshInterval ?? 1500;
protected ServerState? ServerState => MainData?.ServerState; protected ServerState? ServerState => MainData?.ServerState;
protected void NavigateBack() protected void NavigateBack()

View File

@@ -1,14 +1,14 @@
@page "/" @page "/"
@layout ListLayout @layout ListLayout
<MudToolBar DisableGutters="true" Dense="true"> <MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" Title="Add torrent link" /> <MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" Title="Add torrent file" /> <MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" title="Add torrent file" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrents()" /> <TorrentActions RenderType="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrents()" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrent" Title="View torrent details" /> <MudIconButton Icon="@Icons.Material.Outlined.Info" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ShowTorrent" title="View torrent details" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" /> <MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" title="Choose Columns" />
<MudSpacer /> <MudSpacer />
<MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField> <MudTextField Value="SearchText" TextChanged="SearchTextChanged" Immediate="true" DebounceInterval="1000" Placeholder="Filter torrent list" Adornment="Adornment.Start" AdornmentIcon="@Icons.Material.Filled.Search" IconSize="Size.Medium" Class="mt-0"></MudTextField>
</MudToolBar> </MudToolBar>

View File

@@ -170,4 +170,24 @@ td.no-wrap {
.details-tab-contents { .details-tab-contents {
height: calc(100vh - 200px); height: calc(100vh - 200px);
overflow: auto; overflow: auto;
}
.search-list .mud-table-container {
height: calc(100vh - 260px);
}
tr.log-normal td {
color: var(--mud-palette-text-primary) !important;
}
tr.log-info td {
color: var(--mud-palette-info) !important;
}
tr.log-warning td {
color: var(--mud-palette-warning) !important;
}
tr.log-critical td {
color: var(--mud-palette-error) !important;
} }

View File

@@ -887,10 +887,10 @@ namespace Lantean.QBitTorrentClient
{ {
var content = new FormUrlEncodedBuilder() var content = new FormUrlEncodedBuilder()
.AddAllOrPipeSeparated("hashes", all, hashes) .AddAllOrPipeSeparated("hashes", all, hashes)
.Add("enable", value) .Add("value", value)
.ToFormUrlEncodedContent(); .ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("torrents/setFOrceStart", content); var response = await _httpClient.PostAsync("torrents/setForceStart", content);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
} }
@@ -1105,16 +1105,30 @@ namespace Lantean.QBitTorrentClient
private async Task<IReadOnlyList<T>> GetJsonList<T>(HttpContent content) private async Task<IReadOnlyList<T>> GetJsonList<T>(HttpContent content)
{ {
var items = await GetJson<IEnumerable<T>>(content); try
{
var items = await GetJson<IEnumerable<T>>(content);
return items.ToList().AsReadOnly(); return items.ToList().AsReadOnly();
}
catch
{
return [];
}
} }
private async Task<IReadOnlyDictionary<TKey, TValue>> GetJsonDictionary<TKey, TValue>(HttpContent content) where TKey : notnull private async Task<IReadOnlyDictionary<TKey, TValue>> GetJsonDictionary<TKey, TValue>(HttpContent content) where TKey : notnull
{ {
var items = await GetJson<IDictionary<TKey, TValue>>(content); try
{
var items = await GetJson<IDictionary<TKey, TValue>>(content);
return items.AsReadOnly(); return items.AsReadOnly();
}
catch
{
return new Dictionary<TKey, TValue>().AsReadOnly();
}
} }
} }
} }