Add initial RSS implementation

This commit is contained in:
ahjephson
2024-08-28 16:15:59 +01:00
parent ecea664b48
commit 7c4a185b58
41 changed files with 678 additions and 116 deletions

4
.editorconfig Normal file
View File

@@ -0,0 +1,4 @@
[*.cs]
# IDE0290: Use primary constructor
csharp_style_prefer_primary_constructors = false

View File

@@ -9,6 +9,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBitTorrentClient",
EndProject EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMudBlade", "Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}" Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Lantean.QBTMudBlade", "Lantean.QBTMudBlade\Lantean.QBTMudBlade.csproj", "{83BC76CC-D51B-42AF-A6EE-FA400C300098}"
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{1BF1A631-87D7-4039-A701-88C5E0234B63}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
EndProjectSection
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU Debug|Any CPU = Debug|Any CPU

View File

@@ -4,9 +4,6 @@ using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Helpers;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using System;
using Lantean.QBTMudBlade.Pages;
using static MudBlazor.CategoryTypes;
namespace Lantean.QBTMudBlade.Components namespace Lantean.QBTMudBlade.Components
{ {
@@ -26,20 +23,39 @@ namespace Lantean.QBTMudBlade.Components
[Parameter] [Parameter]
public bool IsMenu { get; set; } public bool IsMenu { get; set; }
protected IEnumerable<UIAction> Actions => _actions ?? []; [Parameter]
[EditorRequired]
public Preferences? Preferences { get; set; }
protected IEnumerable<UIAction> Actions => GetActions();
private IEnumerable<UIAction> GetActions()
{
if (_actions is not null)
{
foreach (var action in _actions)
{
if (action.Name != "rss" || Preferences is not null && Preferences.RssProcessingEnabled)
{
yield return action;
}
}
}
}
protected override void OnInitialized() protected override void OnInitialized()
{ {
_actions = _actions =
[ [
new("Statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"), new("statistics", "Statistics", Icons.Material.Filled.PieChart, Color.Default, "/statistics"),
new("Search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"), new("search", "Search", Icons.Material.Filled.Search, Color.Default, "/search"),
new("RSS", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"), new("rss", "RSS", Icons.Material.Filled.RssFeed, Color.Default, "/rss"),
new("Execution Log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"), new("log", "Execution Log", Icons.Material.Filled.List, Color.Default, "/log"),
new("Blocked IPs", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"), new("blocks", "Blocked IPs", Icons.Material.Filled.DisabledByDefault, Color.Default, "/blocks"),
new("Tag Management", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true), new("tags", "Tag Management", Icons.Material.Filled.Label, Color.Default, "/tags", separatorBefore: true),
new("Category Management", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"), new("categories", "Category Management", Icons.Material.Filled.List, Color.Default, "/categories"),
new("Settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true), new("settings", "Settings", Icons.Material.Filled.Settings, Color.Default, "/settings", separatorBefore: true),
new("about", "About", Icons.Material.Filled.Info, Color.Default, "/about"),
]; ];
} }
@@ -66,7 +82,7 @@ namespace Lantean.QBTMudBlade.Components
{ {
await ApiClient.Logout(); await ApiClient.Logout();
NavigationManager.NavigateTo("/login", true); NavigationManager.NavigateTo("/", true);
}); });
} }

View File

@@ -19,12 +19,23 @@ namespace Lantean.QBTMudBlade.Components.Dialogs
[CascadingParameter] [CascadingParameter]
public MudDialogInstance MudDialog { get; set; } = default!; public MudDialogInstance MudDialog { get; set; } = default!;
[Parameter]
public string? Url { get; set; }
protected MudTextField<string?>? UrlsTextField { get; set; } protected MudTextField<string?>? UrlsTextField { get; set; }
protected string? Urls { get; set; } protected string? Urls { get; set; }
protected AddTorrentOptions TorrentOptions { get; set; } = default!; protected AddTorrentOptions TorrentOptions { get; set; } = default!;
protected override void OnInitialized()
{
if (Url is not null)
{
Urls = Url;
}
}
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)
{ {
if (firstRender) if (firstRender)

View File

@@ -20,7 +20,7 @@
<FieldSwitch Label="Ratio" Value="RatioEnabled" ValueChanged="RatioEnabledChanged" Disabled="@(!CustomEnabled)" /> <FieldSwitch Label="Ratio" Value="RatioEnabled" ValueChanged="RatioEnabledChanged" Disabled="@(!CustomEnabled)" />
</MudItem> </MudItem>
<MudItem xs="9"> <MudItem xs="9">
<MudNumericField T="float" Value="Ratio" ValueChanged="RatioChanged" Disabled="@(!(CustomEnabled && RatioEnabled))" Min="1" Max="1024000" Format="F2" Variant="Variant.Outlined" /> <MudNumericField T="float" Value="Ratio" ValueChanged="RatioChanged" Disabled="@(!(CustomEnabled && RatioEnabled))" Min="0" Max="1024000" Step="0.1F" Format="F2" Variant="Variant.Outlined" />
</MudItem> </MudItem>
<MudItem xs="3"> <MudItem xs="3">
<FieldSwitch Label="Total minutes" Value="TotalMinutesEnabled" ValueChanged="TotalMinutesEnabledChanged" Disabled="@(!CustomEnabled)" /> <FieldSwitch Label="Total minutes" Value="TotalMinutesEnabled" ValueChanged="TotalMinutesEnabledChanged" Disabled="@(!CustomEnabled)" />

View File

@@ -22,7 +22,7 @@ namespace Lantean.QBTMudBlade.Components
protected override Task OnErrorAsync(Exception exception) protected override Task OnErrorAsync(Exception exception)
{ {
Logger.LogError(exception, exception.Message); Logger.LogError(exception, "An application error occurred: {message}.", exception.Message);
_exceptions.Add(exception); _exceptions.Add(exception);
if (Disabled) if (Disabled)

View File

@@ -1,3 +1,6 @@
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft"> @if (_isVisible)
<ApplicationActions IsMenu="true" /> {
</MudMenu> <MudMenu Icon="@Icons.Material.Filled.MoreVert" Color="Color.Inherit" Dense="true" AnchorOrigin="Origin.BottomRight" TransformOrigin="Origin.TopLeft">
<ApplicationActions IsMenu="true" Preferences="Preferences" />
</MudMenu>
}

View File

@@ -8,6 +8,10 @@ namespace Lantean.QBTMudBlade.Components
{ {
public partial class Menu public partial class Menu
{ {
private bool _isVisible = false;
private Preferences? _preferences;
[Inject] [Inject]
protected NavigationManager NavigationManager { get; set; } = default!; protected NavigationManager NavigationManager { get; set; } = default!;
@@ -17,6 +21,16 @@ namespace Lantean.QBTMudBlade.Components
[Inject] [Inject]
protected IApiClient ApiClient { get; set; } = default!; protected IApiClient ApiClient { get; set; } = default!;
protected Preferences? Preferences => _preferences;
public void ShowMenu(Preferences? preferences = null)
{
_isVisible = true;
_preferences = preferences;
StateHasChanged();
}
protected async Task ResetWebUI() protected async Task ResetWebUI()
{ {
var preferences = new UpdatePreferences var preferences = new UpdatePreferences

View File

@@ -90,21 +90,21 @@ namespace Lantean.QBTMudBlade.Components
new("firstLastPiecePrio", "Download first and last pieces first", Icons.Material.Filled.Check, Color.Info, CreateCallback(DownloadFirstLast)), new("firstLastPiecePrio", "Download first and last pieces first", Icons.Material.Filled.Check, Color.Info, CreateCallback(DownloadFirstLast)),
new("forceRecheck", "Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck), separatorBefore: true), new("forceRecheck", "Force recheck", Icons.Material.Filled.Loop, Color.Info, CreateCallback(ForceRecheck), separatorBefore: true),
new("forceReannounce", "Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)), new("forceReannounce", "Force reannounce", Icons.Material.Filled.BroadcastOnHome, Color.Info, CreateCallback(ForceReannounce)),
new("queue", "Queue", Icons.Material.Filled.Queue, Color.Transparent, new List<UIAction> new("queue", "Queue", Icons.Material.Filled.Queue, Color.Transparent,
{ [
new("queueTop", "Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)), new("queueTop", "Move to top", Icons.Material.Filled.VerticalAlignTop, Color.Inherit, CreateCallback(MoveToTop)),
new("queueUp", "Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)), new("queueUp", "Move up", Icons.Material.Filled.ArrowUpward, Color.Inherit, CreateCallback(MoveUp)),
new("queueDown", "Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)), new("queueDown", "Move down", Icons.Material.Filled.ArrowDownward, Color.Inherit, CreateCallback(MoveDown)),
new("queueBottom", "Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)), new("queueBottom", "Move to bottom", Icons.Material.Filled.VerticalAlignBottom, Color.Inherit, CreateCallback(MoveToBottom)),
}, separatorBefore: true), ], separatorBefore: true),
new("copy", "Copy", Icons.Material.Filled.FolderCopy, Color.Info, new List<UIAction> new("copy", "Copy", Icons.Material.Filled.FolderCopy, Color.Info,
{ [
new("copyName", "Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))), new("copyName", "Name", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Name))),
new("copyHashv1", "Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))), new("copyHashv1", "Info hash v1", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV1))),
new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))), new("copyHashv2", "Info hash v2", Icons.Material.Filled.Tag, Color.Info, CreateCallback(() => Copy(t => t.InfoHashV2))),
new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))), new("copyMagnet", "Magnet link", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.MagnetUri))),
new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))), new("copyId", "Torrent ID", Icons.Material.Filled.TextFields, Color.Info, CreateCallback(() => Copy(t => t.Hash))),
}), ]),
new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)), new("export", "Export", Icons.Material.Filled.SaveAlt, Color.Info, CreateCallback(Export)),
]; ];
} }

View File

@@ -3,6 +3,7 @@ using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Components.UI; using Lantean.QBTMudBlade.Components.UI;
using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Helpers;
using Lantean.QBTMudBlade.Interop; using Lantean.QBTMudBlade.Interop;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services; using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop; using Microsoft.JSInterop;

View File

@@ -1,5 +1,6 @@
using Blazored.LocalStorage; using Blazored.LocalStorage;
using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Helpers;
using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web; using Microsoft.AspNetCore.Components.Web;
using MudBlazor; using MudBlazor;

View File

@@ -1,5 +1,6 @@
using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models; using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services; using Lantean.QBTMudBlade.Services;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using System.Net; using System.Net;

View File

@@ -5,4 +5,3 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0290:Use primary constructor", Justification = "<Pending>", Scope = "member", Target = "~M:qtmud.Models.Torrent.#ctor(System.String,System.DateTimeOffset,System.Int64,System.Boolean,System.Single,System.String,System.Int64,System.DateTimeOffset,System.String,System.Int64,System.Int64,System.Int64,System.Int64,System.Int64,System.Boolean,System.Boolean,System.String,System.String,System.DateTimeOffset,System.String,System.Single,System.Int32,System.String,System.Int32,System.Int32,System.Int32,System.Int32,System.Single,System.Single,System.Single,System.String,System.DateTimeOffset,System.Int32,System.DateTimeOffset,System.Boolean,System.Int64,System.String,System.Boolean,System.Collections.Generic.IEnumerable{System.String},System.DateTimeOffset,System.Int64,System.String,System.Int64,System.Int64,System.Int64,System.Int64)")]

View File

@@ -64,9 +64,14 @@ namespace Lantean.QBTMudBlade.Helpers
} }
} }
public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient) public static async Task InvokeAddTorrentLinkDialog(this IDialogService dialogService, IApiClient apiClient, string? url = null)
{ {
var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", FormDialogOptions); var parameters = new DialogParameters
{
{ nameof(AddTorrentLinkDialog.Url), url }
};
var result = await dialogService.ShowAsync<AddTorrentLinkDialog>("Download from URLs", parameters, FormDialogOptions);
var dialogResult = await result.Result; var dialogResult = await result.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null) if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{ {

View File

@@ -11,11 +11,13 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" /> <PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" /> <PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.7" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="8.0.8" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.7" PrivateAssets="all" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="8.0.8" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Http" Version="8.0.0" />
<PackageReference Include="MudBlazor" Version="7.5.0" /> <PackageReference Include="MudBlazor" Version="7.6.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="2.0.0" /> <PackageReference Include="MudBlazor.ThemeManager" Version="2.0.0" />
<!-- added to fix vuln in dependency -->
<PackageReference Include="System.Text.Json" Version="8.0.4" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Components;
using Lantean.QBTMudBlade.Helpers; using Lantean.QBTMudBlade.Helpers;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using Lantean.QBTMudBlade.Services; using Lantean.QBTMudBlade.Services;
@@ -28,6 +29,9 @@ namespace Lantean.QBTMudBlade.Layout
[CascadingParameter(Name = "DrawerOpen")] [CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; } public bool DrawerOpen { get; set; }
[CascadingParameter]
public Menu? Menu { get; set; }
protected MainData? MainData { get; set; } protected MainData? MainData { get; set; }
protected string Category { get; set; } = FilterHelper.CATEGORY_ALL; protected string Category { get; set; } = FilterHelper.CATEGORY_ALL;
@@ -85,6 +89,8 @@ namespace Lantean.QBTMudBlade.Layout
_refreshInterval = MainData.ServerState.RefreshInterval; _refreshInterval = MainData.ServerState.RefreshInterval;
IsAuthenticated = true; IsAuthenticated = true;
Menu?.ShowMenu(Preferences);
} }
protected override async Task OnAfterRenderAsync(bool firstRender) protected override async Task OnAfterRenderAsync(bool firstRender)

View File

@@ -21,17 +21,16 @@
</MudBadge> </MudBadge>
} }
<MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" /> <MudSwitch T="bool" Label="Dark Mode" LabelPosition="LabelPosition.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
@if (ShowMenu) <Menu @ref="Menu" />
{
<Menu />
}
</MudAppBar> </MudAppBar>
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right"> <MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" /> <ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer> </MudDrawer>
<CascadingValue Value="Theme"> <CascadingValue Value="Theme">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode"> <CascadingValue Value="IsDarkMode" Name="IsDarkMode">
@Body <CascadingValue Value="Menu">
@Body
</CascadingValue>
</CascadingValue> </CascadingValue>
</CascadingValue> </CascadingValue>
</MudLayout> </MudLayout>

View File

@@ -30,8 +30,6 @@ namespace Lantean.QBTMudBlade.Layout
protected bool ErrorDrawerOpen { get; set; } = false; protected bool ErrorDrawerOpen { get; set; } = false;
protected bool ShowMenu { get; set; } = false;
public Guid Id => Guid.NewGuid(); public Guid Id => Guid.NewGuid();
protected EnhancedErrorBoundary? ErrorBoundary { get; set; } protected EnhancedErrorBoundary? ErrorBoundary { get; set; }
@@ -40,6 +38,8 @@ namespace Lantean.QBTMudBlade.Layout
protected MudThemeProvider MudThemeProvider { get; set; } = default!; protected MudThemeProvider MudThemeProvider { get; set; } = default!;
private Menu Menu { get; set; } = default!;
ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new() ResizeOptions IBrowserViewportObserver.ResizeOptions { get; } = new()
{ {
ReportRate = 50, ReportRate = 50,
@@ -62,11 +62,6 @@ namespace Lantean.QBTMudBlade.Layout
protected override async Task OnParametersSetAsync() protected override async Task OnParametersSetAsync()
{ {
if (!ShowMenu)
{
ShowMenu = await ApiClient.CheckAuthState();
}
var drawerOpen = await LocalStorage.GetItemAsync<bool?>(_drawerOpenStorageKey); var drawerOpen = await LocalStorage.GetItemAsync<bool?>(_drawerOpenStorageKey);
if (drawerOpen is not null) if (drawerOpen is not null)
{ {

View File

@@ -3,7 +3,7 @@
<MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false"> <MudDrawer Open="DrawerOpen" ClipMode="DrawerClipMode.Always" Elevation="2" Overlay="false">
<MudNavMenu> <MudNavMenu>
<ApplicationActions IsMenu="false" /> <ApplicationActions IsMenu="false" Preferences="Preferences" />
</MudNavMenu> </MudNavMenu>
</MudDrawer> </MudDrawer>
<MudMainContent> <MudMainContent>

View File

@@ -1,4 +1,5 @@
using Microsoft.AspNetCore.Components; using Lantean.QBitTorrentClient.Models;
using Microsoft.AspNetCore.Components;
namespace Lantean.QBTMudBlade.Layout namespace Lantean.QBTMudBlade.Layout
{ {
@@ -6,5 +7,8 @@ namespace Lantean.QBTMudBlade.Layout
{ {
[CascadingParameter(Name = "DrawerOpen")] [CascadingParameter(Name = "DrawerOpen")]
public bool DrawerOpen { get; set; } public bool DrawerOpen { get; set; }
[CascadingParameter]
public Preferences? Preferences { get; set; }
} }
} }

View File

@@ -1,16 +1,8 @@
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
namespace Lantean.QBTMudBlade namespace Lantean.QBTMudBlade.Models
{ {
public static class TableHelper
{
public static void CreateColumn(string name)
{
// do it
}
}
public class ColumnDefinition<T> public class ColumnDefinition<T>
{ {
public ColumnDefinition(string header, Func<T, object?> sortSelector, Func<T, string>? formatter = null, string? tdClass = null, int? width = null) public ColumnDefinition(string header, Func<T, object?> sortSelector, Func<T, string>? formatter = null, string? tdClass = null, int? width = null)
@@ -61,25 +53,4 @@ namespace Lantean.QBTMudBlade
return new RowContext<T>(Header, data, Formatter is null ? SortSelector : Formatter); return new RowContext<T>(Header, data, Formatter is null ? SortSelector : Formatter);
} }
} }
public record RowContext<T>
{
private readonly Func<T, object?> _valueGetter;
public RowContext(string headerText, T data, Func<T, object?> valueGetter)
{
HeaderText = headerText;
Data = data;
_valueGetter = valueGetter;
}
public string HeaderText { get; }
public T Data { get; set; }
public object? GetValue()
{
return _valueGetter(Data);
}
}
} }

View File

@@ -1,6 +1,6 @@
namespace Lantean.QBTMudBlade.Models namespace Lantean.QBTMudBlade.Models
{ {
public struct FilterState public readonly struct FilterState
{ {
public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms) public FilterState(string category, Status status, string tag, string tracker, bool useSubcategories, string? terms)
{ {

View File

@@ -9,7 +9,9 @@
public int? LastKnownId { get; set; } public int? LastKnownId { get; set; }
#pragma warning disable IDE0028 // Simplify collection initialization - the SelectedValues of MudSelect has issues with the type being HashSet<string> but it needs to be.
public IEnumerable<string> SelectedTypes { get; set; } = new HashSet<string>(); public IEnumerable<string> SelectedTypes { get; set; } = new HashSet<string>();
#pragma warning restore IDE0028 // Simplify collection initialization
public string? Criteria { get; set; } public string? Criteria { get; set; }
} }

View File

@@ -0,0 +1,24 @@
namespace Lantean.QBTMudBlade.Models
{
public record RowContext<T>
{
private readonly Func<T, object?> _valueGetter;
public RowContext(string headerText, T data, Func<T, object?> valueGetter)
{
HeaderText = headerText;
Data = data;
_valueGetter = valueGetter;
}
public string HeaderText { get; }
public T Data { get; set; }
public object? GetValue()
{
return _valueGetter(Data);
}
}
}

View File

@@ -48,7 +48,7 @@ namespace Lantean.QBTMudBlade.Pages
#if DEBUG #if DEBUG
protected override Task OnInitializedAsync() protected override Task OnInitializedAsync()
{ {
return DoLogin("admin", "V9VpmhCvv"); return DoLogin("admin", "dhT67PMAe");
} }
#endif #endif
} }

View File

@@ -81,21 +81,8 @@ namespace Lantean.QBTMudBlade.Pages
} }
} }
protected async Task NavigateBack() protected void NavigateBack()
{ {
//if (UpdatePreferences is null)
//{
// NavigationManager.NavigateTo("/");
// return;
//}
//await DialogService.ShowConfirmDialog(
// "Unsaved Changed",
// "Are you sure you want to leave without saving your changes?",
// () => NavigationManager.NavigateTo("/"));
await Task.CompletedTask;
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }

View File

@@ -7,10 +7,66 @@
<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" />
} }
<MudIconButton Icon="@Icons.Material.Outlined.Subscriptions" OnClick="NewSubscription" title="New subscription" />
<MudIconButton Icon="@Icons.Material.Outlined.MarkEmailRead" OnClick="MarkAsRead" title="Mark items read" />
<MudIconButton Icon="@Icons.Material.Outlined.Update" OnClick="UpdateAll" title="Update all" />
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.DownloadForOffline" OnClick="EditDownloadRules" title="Edit auto downloading rules" />
<MudDivider Vertical="true" /> <MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">RSS</MudText> <MudText Class="pl-5 no-wrap">RSS</MudText>
</MudToolBar> </MudToolBar>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="details-tab-contents"> <MudContainer MaxWidth="MaxWidth.ExtraExtraLarge">
<p>Coming soon.</p> <MudGrid Class="rss-contents">
</MudContainer> <MudItem xs="4" Style="height: 100%">
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedFeed" SelectedValueChanged="SelectedFeedChanged">
@foreach (var (key, feed) in Items)
{
<MudListItem Icon="@Icons.Material.Filled.Wifi" Text="@feed.Title" Value="@key" />
}
</MudList>
</MudItem>
<MudItem xs="4" Style="height: 100%">
@if (SelectedFeed is not null && SelectedRssItem?.Articles is not null)
{
<MudList T="string" SelectionMode="SelectionMode.SingleSelection" SelectedValue="SelectedArticle" SelectedValueChanged="SelectedArticleChanged">
@foreach (var article in SelectedRssItem.Articles)
{
<MudListItem Text="@article.Title" Value="article.Id" />
}
</MudList>
}
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
<MudItem xs="4" Style="height: 100%">
@if (SelectedFeed is not null && SelectedArticle is not null && SelectedRssArticle is not null)
{
<MudCard>
<MudCardHeader>
<CardHeaderContent>
<MudText Typo="Typo.h6">@SelectedRssArticle.Title</MudText>
</CardHeaderContent>
<CardHeaderActions>
<MudMenu Icon="@Icons.Material.Filled.MoreVert" Dense>
<MudMenuItem Icon="@Icons.Material.Filled.Download" OnClick="c => DownloadItem(SelectedRssArticle.TorrentURL)" title="Download">Download</MudMenuItem>
<MudMenuItem Icon="@Icons.Material.Filled.Link" Href="@SelectedRssArticle.TorrentURL" Target="@SelectedRssArticle.TorrentURL" title="Download">Open torrent URL</MudMenuItem>
</MudMenu>
</CardHeaderActions>
</MudCardHeader>
<MudCardContent>
<MudText Typo="Typo.subtitle2" Style="overflow-wrap: anywhere">@SelectedRssArticle.Date</MudText>
<MudText Typo="Typo.body1">@SelectedRssArticle.Description</MudText>
</MudCardContent>
</MudCard>
}
else
{
<MudSkeleton SkeletonType="SkeletonType.Rectangle" Height="100%" Animation="Animation.False" Width="100%" />
}
</MudItem>
</MudGrid>
</MudContainer>

View File

@@ -1,4 +1,5 @@
using Lantean.QBitTorrentClient; using Lantean.QBitTorrentClient;
using Lantean.QBTMudBlade.Helpers;
using Lantean.QBTMudBlade.Models; using Lantean.QBTMudBlade.Models;
using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components;
using MudBlazor; using MudBlazor;
@@ -31,9 +32,96 @@ namespace Lantean.QBTMudBlade.Pages
protected ServerState? ServerState => MainData?.ServerState; protected ServerState? ServerState => MainData?.ServerState;
protected string? SelectedFeed { get; set; }
protected string? SelectedArticle { get; set; }
public IReadOnlyDictionary<string, QBitTorrentClient.Models.RssItem> Items { get; private set; } = new Dictionary<string, QBitTorrentClient.Models.RssItem>();
protected IReadOnlyList<int> ColumnsSizes => GetColumnSizes();
private IReadOnlyList<int> GetColumnSizes()
{
if (SelectedFeed is null)
{
return [12, 0, 0];
}
if (SelectedFeed is not null && SelectedArticle is null)
{
return [6, 6, 0];
}
return [4, 4, 4];
}
protected QBitTorrentClient.Models.RssItem? SelectedRssItem
{
get
{
if (SelectedFeed == null)
{
return null;
}
Items.TryGetValue(SelectedFeed, out var feed);
return feed;
}
}
protected QBitTorrentClient.Models.RssArticle? SelectedRssArticle
{
get
{
return SelectedRssItem?.Articles?.FirstOrDefault(a => a.Id == SelectedArticle);
}
}
protected void SelectedFeedChanged(string value)
{
SelectedFeed = value;
SelectedArticle = null;
}
protected void SelectedArticleChanged(string value)
{
SelectedArticle = value;
}
protected override async Task OnInitializedAsync()
{
Items = await ApiClient.GetAllRssItems(true);
}
protected async Task DownloadItem(string? url)
{
await DialogService.InvokeAddTorrentLinkDialog(ApiClient, url);
}
protected void NavigateBack() protected void NavigateBack()
{ {
NavigationManager.NavigateTo("/"); NavigationManager.NavigateTo("/");
} }
protected async Task NewSubscription()
{
await Task.CompletedTask;
}
protected async Task MarkAsRead()
{
await Task.CompletedTask;
}
protected async Task UpdateAll()
{
await Task.CompletedTask;
}
protected async Task EditDownloadRules()
{
await Task.CompletedTask;
}
} }
} }

View File

@@ -14,7 +14,7 @@ namespace Lantean.QBTMudBlade.Pages
private int? _searchId; private int? _searchId;
private bool _disposedValue; private bool _disposedValue;
private readonly CancellationTokenSource _timerCancellationToken = new(); private readonly CancellationTokenSource _timerCancellationToken = new();
private int _refreshInterval = 1500; private readonly int _refreshInterval = 1500;
private QBitTorrentClient.Models.SearchResults? _searchResults; private QBitTorrentClient.Models.SearchResults? _searchResults;

View File

@@ -14,8 +14,8 @@ namespace Lantean.QBTMudBlade.Pages
{ {
private bool _disposedValue; private bool _disposedValue;
private static KeyboardEvent _addTorrentFileKey = new KeyboardEvent("a") { AltKey = true }; private static readonly KeyboardEvent _addTorrentFileKey = new("a") { AltKey = true };
private static KeyboardEvent _addTorrentLinkKey = new KeyboardEvent("l") { AltKey = true }; private static readonly KeyboardEvent _addTorrentLinkKey = new("l") { AltKey = true };
[Inject] [Inject]

View File

@@ -20,7 +20,9 @@ namespace Lantean.QBTMudBlade
Uri baseAddress; Uri baseAddress;
#if DEBUG #if DEBUG
#pragma warning disable S1075 // URIs should not be hardcoded - used for debugging only
baseAddress = new Uri("http://localhost:8080"); baseAddress = new Uri("http://localhost:8080");
#pragma warning restore S1075 // URIs should not be hardcoded
#else #else
baseAddress = new Uri(builder.HostEnvironment.BaseAddress); baseAddress = new Uri(builder.HostEnvironment.BaseAddress);
#endif #endif

View File

@@ -69,9 +69,11 @@ namespace Lantean.QBTMudBlade.Services
var serverState = CreateServerState(mainData.ServerState); var serverState = CreateServerState(mainData.ServerState);
var tagState = new Dictionary<string, HashSet<string>>(tags.Count + 2); var tagState = new Dictionary<string, HashSet<string>>(tags.Count + 2)
tagState.Add(FilterHelper.TAG_ALL, torrents.Keys.ToHashSet()); {
tagState.Add(FilterHelper.TAG_UNTAGGED, torrents.Values.Where(t => FilterHelper.FilterTag(t, FilterHelper.TAG_UNTAGGED)).ToHashesHashSet()); { FilterHelper.TAG_ALL, torrents.Keys.ToHashSet() },
{ FilterHelper.TAG_UNTAGGED, torrents.Values.Where(t => FilterHelper.FilterTag(t, FilterHelper.TAG_UNTAGGED)).ToHashesHashSet() }
};
foreach (var tag in tags) foreach (var tag in tags)
{ {
tagState.Add(tag, torrents.Values.Where(t => FilterHelper.FilterTag(t, tag)).ToHashesHashSet()); tagState.Add(tag, torrents.Values.Where(t => FilterHelper.FilterTag(t, tag)).ToHashesHashSet());

View File

@@ -13,22 +13,27 @@ namespace Lantean.QBTMudBlade.Services
public object? LogRequestStart(HttpRequestMessage request) public object? LogRequestStart(HttpRequestMessage request)
{ {
//_logger.LogInformation( #if DEBUG
// "Sending '{Request.Method}' to '{Request.Host}{Request.Path}'", _logger.LogInformation(
// request.Method, "Sending '{Request.Method}' to '{Request.Host}{Request.Path}'",
// request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped), request.Method,
// request.RequestUri!.PathAndQuery); request.RequestUri?.GetComponents(UriComponents.SchemeAndServer, UriFormat.Unescaped),
request.RequestUri!.PathAndQuery);
#endif
return null; return null;
} }
public void LogRequestStop( public void LogRequestStop(
object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed) object? context, HttpRequestMessage request, HttpResponseMessage response, TimeSpan elapsed)
{ {
//_logger.LogInformation( #if DEBUG
// "Received '{Response.StatusCodeInt} {Response.StatusCodeString}' after {Response.ElapsedMilliseconds}ms", _logger.LogInformation(
// (int)response.StatusCode, "Received '{Response.StatusCodeInt} {Response.StatusCodeString}' after {Response.ElapsedMilliseconds}ms",
// response.StatusCode, (int)response.StatusCode,
// elapsed.TotalMilliseconds.ToString("F1")); response.StatusCode,
elapsed.TotalMilliseconds.ToString("F1"));
#endif
} }
public void LogRequestFailed( public void LogRequestFailed(

View File

@@ -218,4 +218,8 @@ td.icon-cell {
td .folder-button { td .folder-button {
padding: 6px 16px 6px 16px !important; padding: 6px 16px 6px 16px !important;
} }
.rss-contents {
height: calc(100vh - 149px);
}

View File

@@ -942,14 +942,146 @@ namespace Lantean.QBitTorrentClient
#region RSS #region RSS
// not implementing RSS right now public async Task AddRssFolder(string path)
{
var content = new FormUrlEncodedBuilder()
.Add("path", path)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/addFolder", content);
response.EnsureSuccessStatusCode();
}
public async Task AddRssFeed(string url, string? path = null)
{
var content = new FormUrlEncodedBuilder()
.Add("url", url)
.AddIfNotNullOrEmpty("path", path)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/addFeed", content);
response.EnsureSuccessStatusCode();
}
public async Task RemoveRssItem(string path)
{
var content = new FormUrlEncodedBuilder()
.Add("path", path)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/removeItem", content);
response.EnsureSuccessStatusCode();
}
public async Task MoveRssItem(string itemPath, string destPath)
{
var content = new FormUrlEncodedBuilder()
.Add("itemPath", itemPath)
.Add("destPath", destPath)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/moveItem", content);
response.EnsureSuccessStatusCode();
}
public async Task<IReadOnlyDictionary<string, RssItem>> GetAllRssItems(bool? withData = null)
{
var content = new QueryBuilder()
.AddIfNotNullOrEmpty("withData", withData);
var response = await _httpClient.GetAsync("rss/items", content);
response.EnsureSuccessStatusCode();
return await GetJsonDictionary<string, RssItem>(response.Content);
}
public async Task MarkRssItemAsRead(string itemPath, string? articleId = null)
{
var content = new FormUrlEncodedBuilder()
.Add("itemPath", itemPath)
.AddIfNotNullOrEmpty("articleId", articleId)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/markAsRead", content);
response.EnsureSuccessStatusCode();
}
public async Task RefreshRssItem(string itemPath)
{
var content = new FormUrlEncodedBuilder()
.Add("itemPath", itemPath)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/refresh", content);
response.EnsureSuccessStatusCode();
}
public async Task SetRssAutoDownloadingRule(string ruleName, AutoDownloadingRule ruleDef)
{
var content = new FormUrlEncodedBuilder()
.Add("ruleName", ruleName)
.Add("ruleName", JsonSerializer.Serialize(ruleDef))
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/setRule", content);
response.EnsureSuccessStatusCode();
}
public async Task RenameRssAutoDownloadingRule(string ruleName, string newRuleName)
{
var content = new FormUrlEncodedBuilder()
.Add("ruleName", ruleName)
.Add("newRuleName", newRuleName)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/renameRule", content);
response.EnsureSuccessStatusCode();
}
public async Task RemoveRssAutoDownloadingRule(string ruleName)
{
var content = new FormUrlEncodedBuilder()
.Add("ruleName", ruleName)
.ToFormUrlEncodedContent();
var response = await _httpClient.PostAsync("rss/removeRule", content);
response.EnsureSuccessStatusCode();
}
public async Task<IReadOnlyDictionary<string, AutoDownloadingRule>> GetAllRssAutoDownloadingRules()
{
var response = await _httpClient.GetAsync("rss/rules");
response.EnsureSuccessStatusCode();
return await GetJsonDictionary<string, AutoDownloadingRule>(response.Content);
}
public async Task<IReadOnlyDictionary<string, IReadOnlyList<string>>> GetRssMatchingArticles(string ruleName)
{
var response = await _httpClient.GetAsync("rss/matchingArticles");
response.EnsureSuccessStatusCode();
var dictionary = await GetJsonDictionary<string, IEnumerable<string>>(response.Content);
return ((IDictionary<string, IReadOnlyList<string>>)dictionary.ToDictionary(d => d.Key, d => d.Value.ToList().AsReadOnly())).AsReadOnly();
}
#endregion RSS #endregion RSS
#region Search #region Search
public async Task<int> StartSearch(string pattern, IEnumerable<string> plugins, string category = "all") public async Task<int> StartSearch(string pattern, IEnumerable<string> plugins, string category = "all")
{ {
var content = new FormUrlEncodedBuilder() var content = new FormUrlEncodedBuilder()

View File

@@ -20,7 +20,7 @@
return this; return this;
} }
public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string value) public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string? value)
{ {
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))
{ {
@@ -30,6 +30,16 @@
return this; return this;
} }
public FormUrlEncodedBuilder AddIfNotNullOrEmpty<T>(string key, T? value) where T : struct
{
if (value.HasValue)
{
_parameters.Add(new KeyValuePair<string, string>(key, value.ToString()!));
}
return this;
}
public FormUrlEncodedContent ToFormUrlEncodedContent() public FormUrlEncodedContent ToFormUrlEncodedContent()
{ {
return new FormUrlEncodedContent(_parameters); return new FormUrlEncodedContent(_parameters);

View File

@@ -172,7 +172,29 @@ namespace Lantean.QBitTorrentClient
#region RSS #region RSS
// not implementing RSS right now Task AddRssFolder(string path);
Task AddRssFeed(string url, string? path = null);
Task RemoveRssItem(string path);
Task MoveRssItem(string itemPath, string destPath);
Task<IReadOnlyDictionary<string, RssItem>> GetAllRssItems(bool? withData = null);
Task MarkRssItemAsRead(string itemPath, string? articleId = null);
Task RefreshRssItem(string itemPath);
Task SetRssAutoDownloadingRule(string ruleName, AutoDownloadingRule ruleDef);
Task RenameRssAutoDownloadingRule(string ruleName, string newRuleName);
Task RemoveRssAutoDownloadingRule(string ruleName);
Task<IReadOnlyDictionary<string, AutoDownloadingRule>> GetAllRssAutoDownloadingRules();
Task<IReadOnlyDictionary<string, IReadOnlyList<string>>> GetRssMatchingArticles(string ruleName);
#endregion RSS #endregion RSS

View File

@@ -0,0 +1,77 @@
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record AutoDownloadingRule
{
[JsonConstructor]
public AutoDownloadingRule(
bool enabled,
string mustContain,
string mustNotContain,
bool useRegex,
string episodeFilter,
bool smartFilter,
IEnumerable<string> previouslyMatchedEpisodes,
IEnumerable<string> affectedFeeds,
int ignoreDays,
string lastMatch,
bool addPaused,
string assignedCategory,
string savePath)
{
Enabled = enabled;
MustContain = mustContain;
MustNotContain = mustNotContain;
UseRegex = useRegex;
EpisodeFilter = episodeFilter;
SmartFilter = smartFilter;
PreviouslyMatchedEpisodes = previouslyMatchedEpisodes;
AffectedFeeds = affectedFeeds;
IgnoreDays = ignoreDays;
LastMatch = lastMatch;
AddPaused = addPaused;
AssignedCategory = assignedCategory;
SavePath = savePath;
}
[JsonPropertyName("enabled")]
public bool Enabled { get; }
[JsonPropertyName("mustContain")]
public string MustContain { get; }
[JsonPropertyName("mustNotContain")]
public string MustNotContain { get; }
[JsonPropertyName("useRegex")]
public bool UseRegex { get; }
[JsonPropertyName("episodeFilter")]
public string EpisodeFilter { get; }
[JsonPropertyName("smartFilter")]
public bool SmartFilter { get; }
[JsonPropertyName("previouslyMatchedEpisodes")]
public IEnumerable<string> PreviouslyMatchedEpisodes { get; }
[JsonPropertyName("affectedFeeds")]
public IEnumerable<string> AffectedFeeds { get; }
[JsonPropertyName("ignoreDays")]
public int IgnoreDays { get; }
[JsonPropertyName("lastMatch")]
public string LastMatch { get; }
[JsonPropertyName("addPaused")]
public bool AddPaused { get; }
[JsonPropertyName("assignedCategory")]
public string AssignedCategory { get; }
[JsonPropertyName("savePath")]
public string SavePath { get; }
}
}

View File

@@ -0,0 +1,57 @@
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public class RssArticle
{
[JsonConstructor]
public RssArticle(
string? category,
string? comments,
string? date,
string? description,
string? id,
string? link,
string? thumbnail,
string? title,
string? torrentURL)
{
Category = category;
Comments = comments;
Date = date;
Description = description;
Id = id;
Link = link;
Thumbnail = thumbnail;
Title = title;
TorrentURL = torrentURL;
}
[JsonPropertyName("category")]
public string? Category { get; set; }
[JsonPropertyName("comments")]
public string? Comments { get; set; }
[JsonPropertyName("date")]
public string? Date { get; set; }
[JsonPropertyName("description")]
public string? Description { get; set; }
[JsonPropertyName("id")]
public string? Id { get; set; }
[JsonPropertyName("link")]
public string? Link { get; set; }
[JsonPropertyName("thumbnail")]
public string? Thumbnail { get; set; }
[JsonPropertyName("title")]
public string? Title { get; set; }
[JsonPropertyName("torrentURL")]
public string? TorrentURL { get; set; }
}
}

View File

@@ -0,0 +1,47 @@
using System.Text.Json.Serialization;
namespace Lantean.QBitTorrentClient.Models
{
public record RssItem
{
[JsonConstructor]
public RssItem(
IReadOnlyList<RssArticle>? articles,
bool hasError,
bool isLoading,
string? lastBuildDate,
string? title,
string uid,
string url)
{
Articles = articles;
HasError = hasError;
IsLoading = isLoading;
LastBuildDate = lastBuildDate;
Title = title;
Uid = uid;
Url = url;
}
[JsonPropertyName("articles")]
public IReadOnlyList<RssArticle>? Articles { get; }
[JsonPropertyName("hasError")]
public bool HasError { get; }
[JsonPropertyName("IsLoading")]
public bool IsLoading { get; }
[JsonPropertyName("lastBuildDate")]
public string? LastBuildDate { get; }
[JsonPropertyName("title")]
public string? Title { get; }
[JsonPropertyName("uid")]
public string Uid { get; }
[JsonPropertyName("url")]
public string Url { get; }
}
}

View File

@@ -22,7 +22,7 @@ namespace Lantean.QBitTorrentClient
return this; return this;
} }
public QueryBuilder AddIfNotNullOrEmpty(string key, string value) public QueryBuilder AddIfNotNullOrEmpty(string key, string? value)
{ {
if (!string.IsNullOrEmpty(value)) if (!string.IsNullOrEmpty(value))
{ {
@@ -32,6 +32,16 @@ namespace Lantean.QBitTorrentClient
return this; return this;
} }
public QueryBuilder AddIfNotNullOrEmpty<T>(string key, T? value) where T : struct
{
if (value.HasValue)
{
_parameters.Add(new KeyValuePair<string, string>(key, value.ToString()!));
}
return this;
}
public string ToQueryString() public string ToQueryString()
{ {
if (_parameters.Count == 0) if (_parameters.Count == 0)