Add better error handling, update toolbars and simplify logging

This commit is contained in:
ahjephson
2024-05-10 14:13:32 +01:00
parent dd7b10cf68
commit 53e934b61c
25 changed files with 543 additions and 265 deletions

View File

@@ -9,4 +9,4 @@
<p role="alert">Sorry, there's nothing at this address.</p>
</LayoutView>
</NotFound>
</Router>
</Router>

View File

@@ -6,7 +6,7 @@
<ButtonTemplate>
<MudButton HtmlTag="label"
Variant="Variant.Filled"
Color="Color.Secondary"
Color="Color.Primary"
StartIcon="@Icons.Material.Filled.CloudUpload"
for="@context.Id">
Choose files
@@ -14,8 +14,8 @@
</ButtonTemplate>
</MudFileUpload>
</MudItem>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption />
</MudGrid>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption="true" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Close</MudButton>

View File

@@ -4,8 +4,8 @@
<MudItem xs="12">
<MudTextField Label="Urls" Lines="10" @bind-Value="Urls" Variant="Variant.Outlined" />
</MudItem>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption />
</MudGrid>
<AddTorrentOptions @ref="TorrentOptions" ShowCookieOption="false" />
</DialogContent>
<DialogActions>
<MudButton OnClick="Cancel">Close</MudButton>

View File

@@ -1,62 +1,68 @@
<MudItem xs="12">
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
</MudItem>
@if (ShowCookieOption)
{
<MudGrid>
<MudItem xs="12">
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
<MudSwitch Label="Additional Options" @bind-Value="Expanded" LabelPosition="LabelPosition.End" />
</MudItem>
}
<MudItem xs="12">
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
@foreach (var category in Categories)
</MudGrid>
<MudCollapse Expanded="Expanded">
<MudGrid>
<MudItem xs="12">
<MudSelect Label="Torrent Management Mode" @bind-Value="TorrentManagementMode" Variant="Variant.Outlined">
<MudSelectItem Value="false">Manual</MudSelectItem>
<MudSelectItem Value="true">Automatic</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudTextField Label="Save files to location" @bind-Value="SavePath" Variant="Variant.Outlined"></MudTextField>
</MudItem>
@if (ShowCookieOption)
{
<MudSelectItem Value="category">@category</MudSelectItem>
<MudItem xs="12">
<MudTextField Label="Cookie" @bind-Value="Cookie" Variant="Variant.Outlined"></MudTextField>
</MudItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Start torrent" @bind-Value="StartTorrent" />
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
<MudSelectItem Value="@("None")">None</MudSelectItem>
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
</MudItem>
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
</MudSelect>
<MudItem xs="12">
<MudFieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<MudTextField Label="Rename" @bind-Value="RenameTorrent" Variant="Variant.Outlined"></MudTextField>
</MudItem>
<MudItem xs="12">
<MudSelect Label="Category" @bind-Value="Category" Variant="Variant.Outlined">
@foreach (var category in Categories)
{
<MudSelectItem Value="category">@category</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Start torrent" @bind-Value="StartTorrent" />
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Add to top of queue" @bind-Value="AddToTopOfQueue" />
</MudItem>
<MudItem xs="12">
<MudSelect Label="Stop condition" @bind-Value="StopCondition" Variant="Variant.Outlined">
<MudSelectItem Value="@("None")">None</MudSelectItem>
<MudSelectItem Value="@("MetadataReceived")">Metadata received</MudSelectItem>
<MudSelectItem Value="@("FilesChecked")">Files checked</MudSelectItem>
</MudSelect>
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Skip hash check" @bind-Value="SkipHashCheck" />
</MudItem>
<MudSelect Label="Content layout" @bind-Value="ContentLayout" Variant="Variant.Outlined">
<MudSelectItem Value="@("Original")">Original</MudSelectItem>
<MudSelectItem Value="@("Subfolder")">Create subfolder</MudSelectItem>
<MudSelectItem Value="@("NoSubfolder")">Don't create subfolder'</MudSelectItem>
</MudSelect>
<MudItem xs="12">
<MudFieldSwitch Label="Download in sequentual order" @bind-Value="DownloadInSequentialOrder" />
</MudItem>
<MudItem xs="12">
<MudFieldSwitch Label="Download first and last pieces first" @bind-Value="DownloadFirstAndLastPiecesFirst" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit download rate" @bind-Value="DownloadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
<MudItem xs="12">
<MudNumericField Label="Limit upload rate" @bind-Value="UploadLimit" Variant="Variant.Outlined" Min="0" />
</MudItem>
</MudGrid>
</MudCollapse>

View File

@@ -12,6 +12,8 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[Parameter]
public bool ShowCookieOption { get; set; }
protected bool Expanded { get; set; }
protected bool TorrentManagementMode { get; set; }
protected string SavePath { get; set; } = default!;

View File

@@ -0,0 +1,34 @@
<MudDialog>
<DialogContent>
<MudGrid>
@if (Exception is null)
{
<MudItem xs="12">
<MudAlert Severity="Severity.Error">
Missing error information.
</MudAlert>
</MudItem>
}
else
{
<MudItem xs="12">
<MudField Label="Message">@Exception.Message</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Source">@Exception.Source</MudField>
</MudItem>
<MudItem xs="12">
<MudField Label="Stack Trace">
<pre class="overflow">
@Exception.StackTrace
</pre>
</MudField>
</MudItem>
}
</MudGrid>
</DialogContent>
<DialogActions>
<MudButton OnClick="Close">Close</MudButton>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,20 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components.Dialogs
{
public partial class ExceptionDialog
{
[CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public Exception? Exception { get; set; }
protected void Close(MouseEventArgs args)
{
MudDialog.Cancel();
}
}
}

View File

@@ -0,0 +1,73 @@
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Rendering;
using System.Collections.ObjectModel;
namespace Lantean.QBTMudBlade.Components
{
public class EnhancedErrorBoundary : ErrorBoundaryBase
{
private readonly ObservableCollection<Exception> _exceptions = [];
public bool HasErrored => CurrentException != null;
[Parameter]
public EventCallback OnClear { get; set; }
protected override Task OnErrorAsync(Exception exception)
{
_exceptions.Add(exception);
return Task.CompletedTask;
}
public async Task RecoverAndClearErrors()
{
Recover();
_exceptions.Clear();
await OnClear.InvokeAsync();
}
public async Task ClearErrors()
{
_exceptions.Clear();
await OnClear.InvokeAsync();
}
public IReadOnlyList<Exception> Errors => _exceptions.AsReadOnly();
protected override void BuildRenderTree(RenderTreeBuilder builder)
{
builder.AddContent(0, ChildContent);
//builder.OpenComponent<CascadingValue<EnhancedErrorBoundary>>(0);
//builder.AddAttribute(1, "Value", this);
//builder.AddContent(2, ChildContent);
//builder.CloseComponent();
// if (CurrentException is null)
// {
// builder.AddContent(0, ChildContent);
// }
// else
// {
// if (ErrorContent is not null)
// {
// builder.AddContent(1, ErrorContent(CurrentException));
// }
// else
// {
// builder.OpenElement(2, "div");
// builder.AddContent(3, "Blazor School Custom Error Boundary.");
// builder.AddContent(4, __innerBuilder =>
// {
// __innerBuilder.OpenElement(5, "button");
// __innerBuilder.AddAttribute(6, "type", "button");
// __innerBuilder.AddAttribute(7, "onclick", RecoverAndClearErrors);
// __innerBuilder.AddContent(8, "Continue");
// __innerBuilder.CloseElement();
// });
// builder.CloseElement();
// }
// }
}
}
}

View File

@@ -0,0 +1,9 @@
<MudList Clickable="true">
<MudListItem OnClick="ClearErrors">Clear Errors</MudListItem>
<MudListItem OnClick="ClearErrorsAndResumeAsync">Clear Errors and Resume</MudListItem>
<MudDivider />
@foreach (var error in Errors)
{
<MudListItem OnClick="@(e => ShowException(error))">@error.Message</MudListItem>
}
</MudList>

View File

@@ -0,0 +1,38 @@
using Lantean.QBTMudBlade.Components.Dialogs;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMudBlade.Components
{
public partial class ErrorDisplay
{
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[Parameter]
[EditorRequired]
public EnhancedErrorBoundary ErrorBoundary { get; set; } = default!;
protected IEnumerable<Exception> Errors => ErrorBoundary.Errors;
protected async Task ShowException(Exception exception)
{
var parameters = new DialogParameters
{
{ nameof(ExceptionDialog.Exception), exception }
};
await DialogService.ShowAsync<ExceptionDialog>("Error Details", parameters, DialogHelper.FormDialogOptions);
}
protected async Task ClearErrors()
{
await ErrorBoundary.ClearErrors();
}
protected async Task ClearErrorsAndResumeAsync()
{
await ErrorBoundary.RecoverAndClearErrors();
}
}
}

View File

@@ -1,4 +1,24 @@
<MudTable T="ContentItem" Hover="true" FixedHeader="true" HeaderClass="table-head-bordered" Breakpoint="Breakpoint.None" Bordered="false"
<MudToolBar DisableGutters="true" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFile" Title="Rename" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" />
<MudDivider Vertical="true" />
<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="DoNotDownloadLessThan80PercentAvailability" OnTouch="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<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="NormalPriorityLessThan80PercentAvailability" OnTouch="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" Title="Filter" />
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" Title="Remove Filter" />
<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>
</MudToolBar>
<MudTable T="ContentItem" Hover="true" FixedHeader="true" HeaderClass="table-head-bordered" Breakpoint="Breakpoint.None" Bordered="false"
MultiSelection="true" Dense="true" SelectOnRowClick="false"
Items="Files"
SelectedItems="SelectedItems"
@@ -7,28 +27,6 @@
RowStyleFunc="RowStyle"
RowClassFunc="RowClass"
AllowUnsorted="false">
<ToolBarContent>
<MudToolBar DisableGutters="true" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFile" Title="Rename" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" />
<MudDivider Vertical="true" />
<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="DoNotDownloadLessThan80PercentAvailability" OnTouch="DoNotDownloadLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="DoNotDownloadCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<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="NormalPriorityLessThan80PercentAvailability" OnTouch="NormalPriorityLessThan80PercentAvailability">Less than 80% Availability</MudMenuItem>
<MudMenuItem OnClick="NormalPriorityCurrentlyFilteredFiles" OnTouch="NormalPriorityCurrentlyFilteredFiles">Currently Filtered Files</MudMenuItem>
</MudMenu>
<MudIconButton Icon="@Icons.Material.Outlined.FilterList" OnClick="ShowFilterDialog" Title="Filter" />
<MudIconButton Icon="@Icons.Material.Outlined.FilterListOff" OnClick="RemoveFilter" Title="Remove Filter" />
</MudToolBar>
<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>
</ToolBarContent>
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())
@@ -73,7 +71,7 @@
@if (context.Data.IsFolder)
{
<MudIconButton ButtonType="ButtonType.Button" Icon="@Icons.Material.Filled.ExpandLess" Class="@("pa-0 " + (ExpandedNodes.Contains(context.Data.Name) ? "rotate-180" : "rotate-90"))" OnClick="@(c => ToggleNode(context.Data, c))"></MudIconButton>
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-2" Style="margin-right: 2px" />
<MudIcon Icon="@Icons.Material.Filled.Folder" Class="pt-2" Style="margin-right: 4px; position: relative; top: 3px" />
}
@context.Data.DisplayName
</div>;

View File

@@ -531,7 +531,7 @@ namespace Lantean.QBTMudBlade.Components
return;
}
var files = FileList.Values.Where(f => f.Availability < value).Select(f => f.Index);
var files = FileList.Values.Where(f => !f.IsFolder && f.Availability < value).Select(f => f.Index);
if (!files.Any())
{
@@ -548,7 +548,7 @@ namespace Lantean.QBTMudBlade.Components
return;
}
var files = GetFiles().Select(f => f.Index);
var files = GetFiles().Where(f => !f.IsFolder).Select(f => f.Index);
if (!files.Any())
{

View File

@@ -1,24 +1,24 @@
@if (Type == ParentType.Toolbar)
@if (Type == RenderType.Toolbar)
{
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true">
@ToolbarContent
</MudToolBar>
}
else if (Type == ParentType.ToolbarContents)
else if (Type == RenderType.ToolbarContents)
{
@ToolbarContent
}
else if (Type == ParentType.MixedToolbar)
else if (Type == RenderType.MixedToolbar)
{
<MudToolBar Dense="true" DisableGutters="true" WrapContent="true">
@MixedToolbarContent
</MudToolBar>
}
else if (Type == ParentType.MixedToolbarContents)
else if (Type == RenderType.MixedToolbarContents)
{
@MixedToolbarContent
}
else if (Type == ParentType.InitialIconsOnly)
else if (Type == RenderType.InitialIconsOnly)
{
@foreach (var option in GetOptions().Take(5))
{
@@ -28,151 +28,143 @@ else if (Type == ParentType.InitialIconsOnly)
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" />
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
}
}
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" Label="Actions" EndIcon="@Icons.Material.Filled.ArrowDropDown" @ref="ActionsMenu">
@foreach (var option in GetOptions().Skip(5))
{
@if (option is Divider)
{
<MudDivider />
}
else if (!option.Children.Any())
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback">
@option.Name
</MudMenuItem>
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color">
<MudMenu Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" DisableElevation="true" DisableRipple="true" Class="sub-menu">
<ActivatorContent>
@option.Name
</ActivatorContent>
<ChildContent>
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</ChildContent>
</MudMenu>
</MudMenuItem>
}
}
</MudMenu>
@Menu(GetOptions().Skip(5));
}
else
{
<MudMenu AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var option in GetOptions())
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback">
@if (option is Divider)
{
<MudDivider />
}
else if (!option.Children.Any())
{
@option.Name
}
else
{
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.LeftClick" Icon="@Icons.Material.Filled.ArrowDropDown" DisableElevation="true" DisableRipple="true" Class="sub-menu">
<ActivatorContent>
@option.Name
</ActivatorContent>
@Menu(GetOptions());
}
<ChildContent>
@code {
private RenderFragment ToolbarContent
{
get
{
return __builder =>
{
foreach (var option in GetOptions())
{
if (option is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
{
if (option.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback">@option.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
}
}
else
{
<MudMenu Icon="@option.Icon" IconColor="@option.Color" Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</ChildContent>
</MudMenu>
</MudMenu>
}
}
</MudMenuItem>
};
}
</MudMenu>
}
}
@code {
private RenderFragment ToolbarContent =>
@<NonRendering>
@foreach (var option in GetOptions())
private RenderFragment MixedToolbarContent
{
get
{
@if (option is Divider)
return __builder =>
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
foreach (var option in GetOptions())
{
if (option is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
{
if (option.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback" Disabled="Disabled">@option.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" Disabled="Disabled" />
}
}
else
{
<MudMenu Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" EndIcon="@Icons.Material.Filled.ArrowDropDown">
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</MudMenu>
}
}
};
}
}
private RenderFragment ChildItem(Action option)
{
return __builder =>
{
if (option is Divider)
{
if (option.Icon is null)
{
<MudButton Color="option.Color" OnClick="option.Callback">@option.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" />
}
<MudDivider />
}
else
{
<MudMenu Icon="@option.Icon" IconColor="@option.Color" Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft">
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</MudMenu>
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback" Disabled="Disabled">@option.Name</MudMenuItem>
}
}
</NonRendering>
;
};
}
private RenderFragment MixedToolbarContent =>
@<NonRendering>
@foreach (var option in GetOptions())
private RenderFragment Menu(IEnumerable<Action> actions)
{
return __builder =>
{
@if (option is Divider)
{
<MudDivider Vertical="true" />
}
else if (!option.Children.Any())
{
if (option.Icon is null)
<MudMenu Dense="true" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" Label="Actions" EndIcon="@Icons.Material.Filled.ArrowDropDown" @ref="ActionsMenu" Disabled="@(!Hashes.Any())">
@foreach (var option in actions)
{
<MudButton Color="option.Color" OnClick="option.Callback">@option.Name</MudButton>
}
else
{
<MudIconButton Title="@option.Name" Icon="@option.Icon" Color="option.Color" OnClick="option.Callback" />
}
}
else
{
<MudMenu Label="@option.Name" title="@option.Name" AnchorOrigin="Origin.BottomLeft" TransformOrigin="Origin.TopLeft" EndIcon="@Icons.Material.Filled.ArrowDropDown">
@foreach (var childItem in option.Children)
@if (option is Divider)
{
@ChildItem(childItem)
<MudDivider />
}
</MudMenu>
}
}
</NonRendering>;
else if (!option.Children.Any())
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback" Disabled="Disabled">
@option.Name
</MudMenuItem>
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color">
<MudMenu Dense="true" AnchorOrigin="Origin.TopRight" TransformOrigin="Origin.TopLeft" ActivationEvent="MouseEvent.MouseOver" Icon="@Icons.Material.Filled.ArrowDropDown" DisableElevation="true" DisableRipple="true" Class="sub-menu">
<ActivatorContent>
@option.Name
</ActivatorContent>
private RenderFragment ChildItem(Action option) =>
@<NonRendering>
@if (option is Divider)
{
<MudDivider />
}
else
{
<MudMenuItem Icon="@option.Icon" IconColor="option.Color" OnClick="option.Callback" OnTouch="option.Callback">@option.Name</MudMenuItem>
}
</NonRendering>;
<ChildContent>
@foreach (var childItem in option.Children)
{
@ChildItem(childItem)
}
</ChildContent>
</MudMenu>
</MudMenuItem>
}
}
</MudMenu>
};
}
}

View File

@@ -39,7 +39,7 @@ namespace Lantean.QBTMudBlade.Components
/// If true this component will render as a <see cref="MudToolBar"/> otherwise will render as a <see cref="MudMenu"/>.
/// </summary>
[Parameter]
public ParentType Type { get; set; }
public RenderType Type { get; set; }
[CascadingParameter]
public MainData MainData { get; set; } = default!;
@@ -49,6 +49,8 @@ namespace Lantean.QBTMudBlade.Components
protected MudMenu? ActionsMenu { get; set; }
protected bool Disabled => !Hashes.Any();
protected async Task Pause()
{
await ApiClient.PauseTorrents(Hashes);
@@ -73,7 +75,9 @@ namespace Lantean.QBTMudBlade.Components
{
savePath = torrent.SavePath;
}
await DialogService.ShowSingleFieldDialog("Set Location", "Location", savePath, v => ApiClient.SetTorrentLocation(v, null, Hashes.ToArray()));
await Task.CompletedTask;
throw new InvalidOperationException("BOoooo");
//await DialogService.ShowSingleFieldDialog("Set Location", "Location", savePath, v => ApiClient.SetTorrentLocation(v, null, Hashes.ToArray()));
}
protected async Task Rename()
@@ -234,13 +238,12 @@ namespace Lantean.QBTMudBlade.Components
private IEnumerable<Action> GetOptions()
{
if (!Hashes.Any())
Torrent? torrent = null;
if (Hashes.Any())
{
return [];
torrent = MainData.Torrents[Hashes.First()];
}
var firstTorrent = MainData.Torrents[Hashes.First()];
var categories = new List<Action>
{
new Action("New", Icons.Material.Filled.Add, Color.Info, EventCallback.Factory.Create(this, AddCategory)),
@@ -255,7 +258,7 @@ namespace Lantean.QBTMudBlade.Components
new Action("Remove All", Icons.Material.Filled.Remove, Color.Error, EventCallback.Factory.Create(this, RemoveTags)),
new Divider()
};
tags.AddRange(MainData.Tags.Select(t => new Action(t, firstTorrent.Tags.Contains(t) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, EventCallback.Factory.Create(this, () => ToggleTag(t)))));
tags.AddRange(MainData.Tags.Select(t => new Action(t, (torrent?.Tags.Contains(t) == true) ? Icons.Material.Filled.CheckBox : Icons.Material.Filled.CheckBoxOutlineBlank, Color.Default, EventCallback.Factory.Create(this, () => ToggleTag(t)))));
var options = new List<Action>
{
@@ -268,11 +271,11 @@ namespace Lantean.QBTMudBlade.Components
new Action("Rename", Icons.Material.Filled.DriveFileRenameOutline, Color.Info, EventCallback.Factory.Create(this, Rename)),
new Action("Category", Icons.Material.Filled.List, Color.Info, categories, true),
new Action("Tags", Icons.Material.Filled.Label, Color.Info, tags, true),
new Action("Automatic Torrent Management", Icons.Material.Filled.Check, firstTorrent.AutomaticTorrentManagement ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)),
new Action("Automatic Torrent Management", Icons.Material.Filled.Check, (torrent?.AutomaticTorrentManagement == true) ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleAutoTMM)),
new Divider(),
new Action("Limit upload rate", Icons.Material.Filled.KeyboardDoubleArrowUp, Color.Info, EventCallback.Factory.Create(this, LimitUploadRate)),
new Action("Limit share ratio", Icons.Material.Filled.Percent, Color.Warning, EventCallback.Factory.Create(this, LimitShareRatio)),
new Action("Super seeding mode", Icons.Material.Filled.Check, firstTorrent.SuperSeeding ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleSuperSeeding)),
new Action("Super seeding mode", Icons.Material.Filled.Check, (torrent?.SuperSeeding == true) ? Color.Info : Color.Transparent, EventCallback.Factory.Create(this, ToggleSuperSeeding)),
new Divider(),
new Action("Force recheck", Icons.Material.Filled.Loop, Color.Info, EventCallback.Factory.Create(this, ForceRecheck)),
new Action("Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, EventCallback.Factory.Create(this, ForceReannounce)),
@@ -304,7 +307,7 @@ namespace Lantean.QBTMudBlade.Components
}
}
public enum ParentType
public enum RenderType
{
/// <summary>
/// Renders toolbar contents without the <see cref="MudToolBar"/> wrapper.

View File

@@ -1,22 +1,33 @@
@inherits LayoutComponentBase
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<EnhancedErrorBoundary @ref="ErrorBoundary" OnClear="Cleared">
<MudThemeProvider />
<MudDialogProvider />
<MudSnackbarProvider />
<PageTitle>qBittorrent Web UI</PageTitle>
<PageTitle>qBittorrent Web UI</PageTitle>
<MudLayout>
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</MudLayout>
<MudLayout>
<MudAppBar Elevation="1">
<MudIconButton Icon="@Icons.Material.Filled.Menu" Color="Color.Inherit" Edge="Edge.Start" OnClick="ToggleDrawer" />
<MudText Typo="Typo.h5" Class="ml-3">qBittorrent Web UI</MudText>
<MudSpacer />
@if (ErrorBoundary?.Errors.Count > 0)
{
<MudBadge Content="@(ErrorBoundary?.Errors.Count ?? 0)" Color="Color.Error" Overlap="true">
<MudIconButton Icon="@Icons.Material.Filled.Error" Color="Color.Default" OnClick="ToggleErrorDrawer" />
</MudBadge>
}
@if (ShowMenu)
{
<Menu />
}
</MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</MudLayout>
</EnhancedErrorBoundary>

View File

@@ -1,5 +1,7 @@
using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
using MudBlazor;
using MudBlazor.Services;
@@ -20,10 +22,14 @@ namespace Lantean.QBTMudBlade.Layout
protected bool DrawerOpen { get; set; } = true;
protected bool ErrorDrawerOpen { get; set; } = false;
protected bool ShowMenu { get; set; } = false;
public Guid Id => Guid.NewGuid();
protected EnhancedErrorBoundary? ErrorBoundary { get; set; }
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
{
ReportRate = 50,
@@ -66,6 +72,16 @@ namespace Lantean.QBTMudBlade.Layout
await InvokeAsync(StateHasChanged);
}
protected void ToggleErrorDrawer()
{
ErrorDrawerOpen = !ErrorDrawerOpen;
}
protected void Cleared()
{
ErrorDrawerOpen = false;
}
protected virtual async Task DisposeAsync(bool disposing)
{
if (!_disposedValue)

View File

@@ -49,7 +49,7 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG
protected override async Task OnInitializedAsync()
{
await DoLogin("admin", "p22bMTJDK");
await DoLogin("admin", "a8hbfvNP2");
}
#endif
}

View File

@@ -1,6 +1,17 @@
@page "/"
@layout ListLayout
<MudToolBar DisableGutters="true" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" Title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" Title="Add torrent file" />
<MudDivider Vertical="true" />
<TorrentActions Type="RenderType.InitialIconsOnly" Hashes="GetSelectedTorrents()" />
<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.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" />
<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>
</MudToolBar>
<MudTable
Items="OrderedTorrents"
T="Torrent"
@@ -23,24 +34,6 @@
RowStyleFunc="RowStyle"
Virtualize="true"
Class="torrent-list">
<ToolBarContent>
<MudToolBar DisableGutters="true" Dense="true">
<MudIconButton Icon="@Icons.Material.Outlined.AddLink" OnClick="AddTorrentLink" Title="Add torrent link" />
<MudIconButton Icon="@Icons.Material.Outlined.AddCircle" OnClick="AddTorrentFile" Title="Add torrent file" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Delete" Color="Color.Inherit" Disabled="@(!ToolbarButtonsEnabled)" OnClick="RemoveTorrents" Title="Remove selected torrents" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.PlayArrow" Disabled="@(!ToolbarButtonsEnabled)" OnClick="ResumeTorrents" Title="Resume selected torrents" />
<MudIconButton Icon="@Icons.Material.Outlined.Pause" Disabled="@(!ToolbarButtonsEnabled)" OnClick="PauseTorrents" Title="Pause selected torrents" />
<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.ViewColumn" Color="Color.Inherit" OnClick="ColumnOptions" Title="Choose Columns" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Settings" Color="Color.Inherit" OnClick="Options" Title="View Application Options" />
</MudToolBar>
<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>
</ToolBarContent>
<ColGroup>
<col style="width: 30px" />
@foreach (var column in GetColumns())

View File

@@ -70,6 +70,11 @@ namespace Lantean.QBTMudBlade.Pages
}
}
_sortSelector ??= _columns.First(c => c.Enabled).SortSelector;
if (SelectedTorrent is not null && Torrents is not null && !Torrents.Contains(SelectedTorrent))
{
SelectedTorrent = null;
}
}
private IEnumerable<Torrent>? GetOrderedTorrents()

View File

@@ -26,11 +26,15 @@ namespace Lantean.QBTMudBlade
#endif
builder.Services.AddTransient<CookieHandler>();
builder.Services.AddScoped<HttpLogger>();
builder.Services
.AddScoped(sp => sp
.GetRequiredService<IHttpClientFactory>()
.CreateClient("API"))
.AddHttpClient("API", client => client.BaseAddress = new Uri(baseAddress, "/api/v2/")).AddHttpMessageHandler<CookieHandler>();
.AddHttpClient("API", client => client.BaseAddress = new Uri(baseAddress, "/api/v2/"))
.AddHttpMessageHandler<CookieHandler>()
.RemoveAllLoggers()
.AddLogger<HttpLogger>(wrapHandlersPipeline: true);
builder.Services.AddScoped<ApiClient>();
builder.Services.AddScoped<IApiClient, ApiClient>();
@@ -39,6 +43,12 @@ namespace Lantean.QBTMudBlade
builder.Services.AddBlazoredLocalStorage();
builder.Services.AddSingleton<IClipboardService, ClipboardService>();
#if DEBUG
builder.Logging.SetMinimumLevel(LogLevel.Information);
#else
builder.Logging.SetMinimumLevel(LogLevel.Error);
#endif
await builder.Build().RunAsync();
}
}

View File

@@ -1,5 +1,6 @@
using Lantean.QBTMudBlade.Models;
using MudBlazor;
using System.Linq;
using System.Reflection;
using System.Threading.Channels;
@@ -173,8 +174,8 @@ namespace Lantean.QBTMudBlade.Services
{
foreach (var hash in mainData.TorrentsRemoved)
{
torrentList.Torrents.Remove(hash);
RemoveTorrentFromStates(torrentList, hash);
torrentList.Torrents.Remove(hash);
}
}
@@ -279,12 +280,20 @@ namespace Lantean.QBTMudBlade.Services
torrentList.TagState[FilterHelper.TAG_UNTAGGED].AddIfTrueOrRemove(hash, FilterHelper.FilterTag(torrent, FilterHelper.TAG_UNTAGGED));
foreach (var tag in torrentList.Tags)
{
if (!torrentList.TagState.ContainsKey(tag))
{
torrentList.TagState.Add(tag, []);
}
torrentList.TagState[tag].AddIfTrueOrRemove(hash, FilterHelper.FilterTag(torrent, tag));
}
torrentList.CategoriesState[FilterHelper.CATEGORY_UNCATEGORIZED].AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, FilterHelper.CATEGORY_UNCATEGORIZED, torrentList.ServerState.UseSubcategories));
foreach (var category in torrentList.Categories.Keys)
{
if (!torrentList.CategoriesState.ContainsKey(category))
{
torrentList.CategoriesState.Add(category, []);
}
torrentList.CategoriesState[category].AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
@@ -296,6 +305,10 @@ namespace Lantean.QBTMudBlade.Services
torrentList.TrackersState[FilterHelper.TRACKER_TRACKERLESS].AddIfTrueOrRemove(hash, FilterHelper.FilterTracker(torrent, FilterHelper.TRACKER_TRACKERLESS));
foreach (var tracker in torrentList.Trackers.Keys)
{
if (!torrentList.TrackersState.ContainsKey(tracker))
{
torrentList.TrackersState.Add(tracker, []);
}
torrentList.TrackersState[tracker].AddIfTrueOrRemove(hash, FilterHelper.FilterTracker(torrent, tracker));
}
}

View File

@@ -0,0 +1,49 @@
using Microsoft.Extensions.Http.Logging;
namespace Lantean.QBTMudBlade.Services
{
public class HttpLogger : IHttpClientLogger
{
private readonly ILogger<HttpLogger> _logger;
public HttpLogger(ILogger<HttpLogger> logger)
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
public object? LogRequestStart(HttpRequestMessage request)
{
//_logger.LogInformation(
// "Sending '{Request.Method}' to '{Request.Host}{Request.Path}'",
// request.Method,
// request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped),
// request.RequestUri!.PathAndQuery);
return null;
}
public void LogRequestStop(
object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
{
//_logger.LogInformation(
// "Received '{Response.StatusCodeInt} {Response.StatusCodeString}' after {Response.ElapsedMilliseconds}ms",
// (int)response.StatusCode,
// response.StatusCode,
// elapsed.TotalMilliseconds.ToString("F1"));
}
public void LogRequestFailed(
object? context,
HttpRequestMessage request,
HttpResponseMessage? response,
Exception exception,
TimeSpan elapsed)
{
_logger.LogError(
exception,
"Request towards '{Request.Host}{Request.Path}' failed after {Response.ElapsedMilliseconds}ms",
request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped),
request.RequestUri!.PathAndQuery,
elapsed.TotalMilliseconds.ToString("F1"));
}
}
}

View File

@@ -148,4 +148,10 @@ td.no-wrap {
top: -2px;
right: -5px;
transform: rotate(270deg);
}
}
.overflow {
overflow: auto;
height: 200px;
width: 100%;
}

View File

@@ -23,7 +23,7 @@
<div class="loading-progress-text"></div>
</div>
<div id="blazor-error-ui">
<div id="blazor-error-ui" data-nosnippet>
An unhandled error has occurred.
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>

View File

@@ -476,7 +476,7 @@ namespace Lantean.QBitTorrentClient
{
foreach (var (name, stream) in torrents)
{
content.Add(new StreamContent(stream), name);
content.Add(new StreamContent(stream), "torrents", name);
}
}
if (savePath is not null)