21 Commits

Author SHA1 Message Date
ahjephson
fc51505e3f Start plugin fix 2025-10-18 12:26:13 +01:00
ahjephson
1cf9f97187 Merge tag '1.1.0' into develop
1.1.0
2025-05-30 15:46:03 +01:00
ahjephson
4f9129fd46 Merge branch 'release/1.1.0' 2025-05-30 15:45:32 +01:00
ahjephson
9a9d2c2ee2 Update packages 2025-05-30 15:43:22 +01:00
ahjephson
736bc46745 Merge pull request #2 from lantean-code/feature/fix-statuses
Fix Paused/Stopped Duplicate
2025-05-30 14:19:34 +01:00
ahjephson
23ae19c4c7 Update readme.md 2025-05-20 13:35:59 +01:00
ahjephson
603470eb30 Merge pull request #3 from lantean-code/feature/fix-relative-resources
Fix Reverse Proxy Issue
2025-05-20 13:24:53 +01:00
ahjephson
27c2406340 Fixes #1 2025-04-22 14:08:55 +01:00
ahjephson
4578dcc11f FIx issue with duplicate paused/stopped status lists when handling v4/5 differences 2025-04-22 14:03:33 +01:00
ahjephson
3215fa3936 Merge tag '1.0.2' into develop
1.0.2
2025-03-22 13:52:41 +00:00
ahjephson
78e62f31d0 Merge branch 'hotfix/1.0.2' 2025-03-22 13:52:33 +00:00
ahjephson
e23842fcb0 Fix invalid exception being caught. 2025-03-22 13:51:44 +00:00
ahjephson
411c7f87cc Merge tag '1.0.1' into develop
1.0.1
2025-02-10 08:57:20 +00:00
ahjephson
4098f8f5a9 Merge branch 'hotfix/1.0.1' 2025-02-10 08:57:01 +00:00
ahjephson
12f81c5978 Fix issue with TorrentActions treating actions as all downloaded. 2025-02-10 08:55:46 +00:00
ahjephson
717738d720 Update readme.md 2025-02-07 13:24:25 +00:00
ahjephson
885c34c8cf Update readme.md 2025-02-07 13:10:15 +00:00
ahjephson
ef3c68a6aa Merge tag '1.0.0' into develop
1.0.0
2025-02-07 13:02:16 +00:00
ahjephson
a29e64fc1b Merge branch 'release/1.0.0' 2025-02-07 13:01:36 +00:00
ahjephson
e55955c75e Fix small screen issues 2025-02-07 11:49:37 +00:00
ahjephson
c54f73a517 Merge tag '0.2.0' into develop
0.2.0
2025-02-07 09:49:18 +00:00
27 changed files with 425 additions and 193 deletions

View File

@@ -4,24 +4,20 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="7.1.0" AllowedVersions="[5.0.0,7.*.*)" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
<PackageReference Include="MudBlazor" Version="8.2.0" />
<PackageReference Include="AwesomeAssertions" Version="9.2.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="18.0.0" />
<PackageReference Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1">
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.5">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="System.Net.Http" Version="4.3.4" />
</ItemGroup>
<ItemGroup>

View File

@@ -21,7 +21,7 @@ namespace Lantean.QBTMud.Test
Test2(a => a.Name);
}
private void Test2(Expression<Func<TestClass, object>> expr)
private void Test2(Expression<Func<TestClass, object?>> expr)
{
var body = expr.Body;
}
@@ -38,7 +38,7 @@ namespace Lantean.QBTMud.Test
var l = Expression.Lambda<Func<TestClass, object>>(convertExpression, expression);
Expression<Func<TestClass, object>> expr2 = a => a.Name;
Expression<Func<TestClass, object?>> expr2 = a => a.Name;
var x = l.Compile();
var res = (long)x(new TestClass { Name = "Name", Value = 12 });
@@ -58,9 +58,9 @@ namespace Lantean.QBTMud.Test
public class TestClass
{
public string Name { get; set; }
public string? Name { get; set; }
public string Description { get; set; }
public string? Description { get; set; }
public long Value { get; set; }
}

View File

@@ -0,0 +1,20 @@
<MudDialog>
<DialogContent>
<MudGrid>
<MudItem xs="12">
<MudList T="string">
<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>
<MudDivider />
@foreach (var plugin in Plugins)
{
var pluginRef = plugin;
<MudListItem Icon="@GetIcon(pluginRef.FullName)" IconColor="Color.Default" OnClick="@(e => SetPlugin(pluginRef))">@pluginRef.Name</MudListItem>
}
</MudList>
</MudItem>
</MudGrid>
</DialogContent>
<DialogActions>
</DialogActions>
</MudDialog>

View File

@@ -0,0 +1,72 @@
using Lantean.QBitTorrentClient;
using Lantean.QBitTorrentClient.Models;
using Lantean.QBTMud.Helpers;
using Microsoft.AspNetCore.Components;
using MudBlazor;
namespace Lantean.QBTMud.Components.Dialogs
{
public partial class SearchPluginsDialog
{
[Inject]
protected IApiClient ApiClient { get; set; } = default!;
[Inject]
protected IDialogService DialogService { get; set; } = default!;
[CascadingParameter]
IMudDialogInstance MudDialog { get; set; } = default!;
protected HashSet<SearchPlugin> Plugins { get; set; } = [];
protected IList<string> TorrentCategories { get; private set; } = [];
protected override async Task OnInitializedAsync()
{
Plugins = [.. (await ApiClient.GetSearchPlugins())];
}
protected string GetIcon(string tag)
{
return Icons.Material.Filled.PlusOne;
}
protected async Task SetPlugin(QBitTorrentClient.Models.SearchPlugin plugin)
{
await InvokeAsync(StateHasChanged);
}
protected async Task AddCategory()
{
var addedCategoy = await DialogService.InvokeAddCategoryDialog(ApiClient);
if (addedCategoy is null)
{
return;
}
await ApiClient.SetTorrentCategory(addedCategoy, Hashes);
Plugins.Add(addedCategoy);
await GetTorrentCategories();
}
protected async Task RemoveCategory()
{
await ApiClient.RemoveTorrentCategory(Hashes);
await GetTorrentCategories();
}
protected Task CloseDialog()
{
MudDialog.Close();
return Task.CompletedTask;
}
protected void Cancel()
{
MudDialog.Cancel();
}
}
}

View File

@@ -2,6 +2,7 @@
<MudMenuItem Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileContextMenu">Rename</MudMenuItem>
</ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
<MudIconButton Icon="@Icons.Material.Filled.DriveFileRenameOutline" OnClick="RenameFileToolbar" title="Rename" />
<MudDivider Vertical="true" />
@@ -22,6 +23,7 @@
<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>
</div>
<DynamicTable
@ref="Table"

View File

@@ -12,10 +12,7 @@ namespace Lantean.QBTMud.Components
{
public partial class TorrentActions : IAsyncDisposable
{
private const int _defaultVersion = 5;
private bool _disposedValue;
private int? _version;
private List<UIAction>? _actions;
@@ -74,30 +71,7 @@ namespace Lantean.QBTMud.Components
protected bool OverlayVisible { get; set; }
protected int MajorVersion
{
get
{
if (_version is not null)
{
return _version.Value;
}
if (string.IsNullOrEmpty(Version))
{
return _defaultVersion;
}
if (!System.Version.TryParse(Version.Replace("v", ""), out var version))
{
return _defaultVersion;
}
_version = version.Major;
return _version.Value;
}
}
protected int MajorVersion => VersionHelper.GetMajorVersion(Version);
protected override void OnInitialized()
{
@@ -441,7 +415,7 @@ namespace Lantean.QBTMud.Components
thereAreFirstLastPiecePrio = true;
}
if (torrent.Progress > 0.999999) // not downloaded
if (torrent.Progress < 0.999999) // not downloaded
{
allAreDownloaded = false;
}

View File

@@ -435,5 +435,22 @@ namespace Lantean.QBTMud.Helpers
await dialogService.ShowAsync<SubMenuDialog>(parent.Text, parameters, FormDialogOptions);
}
public static async Task<QBitTorrentClient.Models.SearchPlugin?> ShowSearchPluginsDialog(this IDialogService dialogService)
{
var parameters = new DialogParameters
{
{ nameof(SearchPluginsDialog.Hashes), "" },
};
var result = await dialogService.ShowAsync<SearchPluginsDialog>("Search Plugins", parameters, FormDialogOptions);
var dialogResult = await result.Result;
if (dialogResult is null || dialogResult.Canceled || dialogResult.Data is null)
{
return null;
}
return (QBitTorrentClient.Models.SearchPlugin)dialogResult.Data;
}
}
}

View File

@@ -37,7 +37,7 @@ namespace Lantean.QBTMud.Helpers
{
time = TimeSpan.FromSeconds(seconds.Value);
}
catch (OverflowException)
catch
{
return "∞";
}
@@ -129,7 +129,7 @@ namespace Lantean.QBTMud.Helpers
return "";
}
return Size(size);
return Size(size, prefix, suffix);
}
/// <summary>

View File

@@ -0,0 +1,34 @@

namespace Lantean.QBTMud.Helpers
{
internal static class VersionHelper
{
private static int? _version;
private const int _defaultVersion = 5;
public static int DefaultVersion => _defaultVersion;
public static int GetMajorVersion(string? version)
{
if (_version is not null)
{
return _version.Value;
}
if (string.IsNullOrEmpty(version))
{
return _defaultVersion;
}
if (!Version.TryParse(version?.Replace("v", ""), out var theVersion))
{
return _defaultVersion;
}
_version = theVersion.Major;
return _version.Value;
}
}
}

View File

@@ -4,21 +4,19 @@
<TargetFramework>net9.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CompressionEnabled>false</CompressionEnabled>
<LangVersion>12</LangVersion>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<CompressionEnabled>false</CompressionEnabled>
<LangVersion>12</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.1" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.1" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.1" />
<PackageReference Include="MudBlazor" Version="8.2.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
<!-- added to fix vuln in dependency -->
<PackageReference Include="System.Text.Json" Version="9.0.1" />
<PackageReference Include="Blazored.LocalStorage" Version="4.5.0" />
<PackageReference Include="ByteSize" Version="2.1.2" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly" Version="9.0.10" />
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.DevServer" Version="9.0.10" PrivateAssets="all" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.10" />
<PackageReference Include="MudBlazor" Version="8.13.0" />
<PackageReference Include="MudBlazor.ThemeManager" Version="3.0.0" />
</ItemGroup>
<ItemGroup>

View File

@@ -36,23 +36,23 @@
</CascadingValue>
</CascadingValue>
</CascadingValue>
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten);">
<MudAppBar Bottom="true" Fixed="true" Elevation="0" Dense="true" Style="background-color: var(--mud-palette-dark-lighten); z-index: 900">
@if (MainData?.LostConnection == true)
{
<MudText Class="mx-2 mb-1" Color="Color.Error">qBittorrent client is not reachable</MudText>
<MudText Class="mx-2 mb-1 d-none d-sm-flex" Color="Color.Error">qBittorrent client is not reachable</MudText>
}
<MudSpacer />
<MudText Class="mx-2 mb-1">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" />
<MudText Class="mx-2 mb-1">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">@DisplayHelpers.Size(MainData?.ServerState.FreeSpaceOnDisk, "Free space: ")</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
<MudText Class="mx-2 mb-1 d-none d-sm-flex">DHT @(MainData?.ServerState.DHTNodes ?? 0) nodes</MudText>
<MudDivider Vertical="true" Class="d-none d-sm-flex" />
@{
var (icon, colour) = GetConnectionIcon(MainData?.ServerState.ConnectionStatus);
}
<MudIcon Class="mx-1 mb-1" Icon="@icon" Color="@colour" Title="MainData?.ServerState.ConnectionStatus" />
<MudDivider Vertical="true" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="mx-1 mb-1" Icon="@Icons.Material.Outlined.Speed" Color="@((MainData?.ServerState.UseAltSpeedLimits ?? false) ? Color.Error : Color.Success)" />
<MudDivider Vertical="true" />
<MudDivider Vertical="true" Class="" />
<MudIcon Class="ml-1 mb-1" Icon="@Icons.Material.Filled.KeyboardDoubleArrowDown" Color="Color.Success" />
<MudText Class="mr-1 mb-1">
@DisplayHelpers.Size(MainData?.ServerState.DownloadInfoSpeed, null, "/s")

View File

@@ -83,7 +83,7 @@ namespace Lantean.QBTMud.Layout
Preferences = await ApiClient.GetApplicationPreferences();
Version = await ApiClient.GetApplicationVersion();
var data = await ApiClient.GetMainData(_requestId);
MainData = DataManager.CreateMainData(data);
MainData = DataManager.CreateMainData(data, Version);
_requestId = data.ResponseId;
_refreshInterval = MainData.ServerState.RefreshInterval;
@@ -128,7 +128,7 @@ namespace Lantean.QBTMud.Layout
if (MainData is null || data.FullUpdate)
{
MainData = DataManager.CreateMainData(data);
MainData = DataManager.CreateMainData(data, Version);
}
else
{

View File

@@ -23,16 +23,15 @@
<MudSwitch T="bool" Label="Dark Mode" LabelPlacement="Placement.End" Value="IsDarkMode" ValueChanged="DarkModeChanged" Class="pl-3" />
<Menu @ref="Menu" />
</MudAppBar>
@if (IsDebug)
{
<MudDrawer Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
}
<MudDrawer @bind-Open="ErrorDrawerOpen" ClipMode="DrawerClipMode.Docked" Elevation="2" Anchor="Anchor.Right">
<ErrorDisplay ErrorBoundary="ErrorBoundary" />
</MudDrawer>
<CascadingValue Value="Theme">
<CascadingValue Value="IsDarkMode" Name="IsDarkMode">
<CascadingValue Value="Menu">
@Body
<CascadingValue Value="DrawerOpen" Name="DrawerOpen">
@Body
</CascadingValue>
</CascadingValue>
</CascadingValue>
</CascadingValue>

View File

@@ -13,9 +13,6 @@ namespace Lantean.QBTMud.Layout
private bool _disposedValue;
[Inject]
protected NavigationManager NavigationManager { get; set; } = default!;
[Inject]
private IBrowserViewportService BrowserViewportService { get; set; } = default!;
@@ -44,12 +41,6 @@ namespace Lantean.QBTMud.Layout
protected MudTheme Theme { get; set; }
#if DEBUG
private bool IsDebug { get; } = true;
#else
private bool IsDebug { get; } = false;
#endif
public MainLayout()
{
Theme = new MudTheme();
@@ -84,21 +75,21 @@ namespace Lantean.QBTMud.Layout
{
IsDarkMode = isDarkMode.Value;
}
await MudThemeProvider.WatchSystemPreference(OnSystemPreferenceChanged);
await MudThemeProvider.WatchSystemDarkModeAsync(OnSystemDarkModeChanged);
await BrowserViewportService.SubscribeAsync(this, fireImmediately: true);
await InvokeAsync(StateHasChanged);
}
}
protected Task OnSystemPreferenceChanged(bool value)
protected Task OnSystemDarkModeChanged(bool value)
{
IsDarkMode = value;
return Task.CompletedTask;
}
public Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
public async Task NotifyBrowserViewportChangeAsync(BrowserViewportEventArgs browserViewportEventArgs)
{
if (browserViewportEventArgs.Breakpoint == Breakpoint.Sm && DrawerOpen)
if (browserViewportEventArgs.Breakpoint <= Breakpoint.Sm)
{
DrawerOpen = false;
}
@@ -107,7 +98,17 @@ namespace Lantean.QBTMud.Layout
DrawerOpen = true;
}
return Task.CompletedTask;
if (ErrorBoundary?.Errors.Count > 0)
{
ErrorDrawerOpen = true;
}
else
{
await Task.Delay(250);
ErrorDrawerOpen = false;
}
await InvokeAsync(StateHasChanged);
}
protected void ToggleErrorDrawer()

View File

@@ -11,7 +11,8 @@
Dictionary<string, HashSet<string>> tagState,
Dictionary<string, HashSet<string>> categoriesState,
Dictionary<string, HashSet<string>> statusState,
Dictionary<string, HashSet<string>> trackersState)
Dictionary<string, HashSet<string>> trackersState,
int majorVersion)
{
Torrents = torrents.ToDictionary();
Tags = tags.ToHashSet();
@@ -22,6 +23,7 @@
CategoriesState = categoriesState;
StatusState = statusState;
TrackersState = trackersState;
MajorVersion = majorVersion;
}
public Dictionary<string, Torrent> Torrents { get; }
@@ -36,5 +38,6 @@
public Dictionary<string, HashSet<string>> TrackersState { get; }
public string? SelectedTorrentHash { get; set; }
public bool LostConnection { get; set; }
public int MajorVersion { get; }
}
}

View File

@@ -8,6 +8,7 @@
Completed,
Resumed,
Paused,
Stopped,
Active,
Inactive,
Stalled,
@@ -15,6 +16,6 @@
StalledDownloading,
Checking,
Errored,
Stopped
}
}

View File

@@ -12,89 +12,96 @@
<MudTabs Elevation="2" ApplyEffectsToContainer="true">
<MudTabPanel Text="About">
<div class="d-flex gap-4">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6" Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop" Height="162" Width="94" />
<MudGrid Class="mx-0 mt-0 mb-3">
<MudItem xs="12">
<div class="d-flex gap-3">
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None" Alt="QBT" Height="32" Width="32" /><MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudGrid Class="mt-0 mb-4">
<MudItem xs="12" sm="3" md="2" lg="2" xl="1" Class="d-flex justify-center">
<MudImage Src="images/mascot.png" Alt="Mascot" Class="ma-6"
Fluid ObjectFit="ObjectFit.None" ObjectPosition="ObjectPosition.LeftTop"
Height="162" Width="94" />
</MudItem>
<MudItem xs="12" sm="9" md="10" lg="10" xl="11">
<div class="d-flex flex-column gap-2">
<div class="d-flex gap-3 align-items-center">
<MudImage Src="images/qbittorrent32.png" Fluid ObjectFit="ObjectFit.None"
Alt="QBT" Height="32" Width="32" />
<MudText Typo="Typo.h6">qBittorrent @QBittorrentVersion</MudText>
</div>
<MudText Typo="Typo.body1">
An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.
</MudText>
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Home Page: </MudText>
<MudLink Href="https://www.qbittorrent.org" Target="_blank" Class="ms-2">
qbittorrent.org
</MudLink>
</div>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Bug Tracker: </MudText>
<MudLink Href="https://bugs.qbittorrent.org" Target="_blank" Class="ms-2">
bugs.qbittorrent.org
</MudLink>
</div>
<div class="d-flex flex-wrap">
<MudText Typo="Typo.body1" Class="fw-bold">Forum: </MudText>
<MudLink Href="https://forum.qbittorrent.org" Target="_blank" Class="ms-2">
forum.qbittorrent.org
</MudLink>
</div>
</div>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body1">An advanced BitTorrent client programmed in C++, based on Qt toolkit and libtorrent-rasterbar.</MudText>
</MudItem>
<MudItem xs="12">
<MudText Typo="Typo.body1">Copyright © 2006-2024 The qBittorrent project</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Home Page</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://www.qbittorrent.org" Target="https://www.qbittorrent.org">https://www.qbittorrent.org</MudLink>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Bug Tracker</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://bugs.qbittorrent.org" Target="https://bugs.qbittorrent.org">https://bugs.qbittorrent.org</MudLink>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Forum</MudText>
</MudItem>
<MudItem xs="10">
<MudLink Href="https://forum.qbittorrent.org" Target="https://forum.qbittorrent.org">https://forum.qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
</div>
</MudContainer>
</MudTabPanel>
<MudTabPanel Text="Authors">
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="mt-3">
<MudText Typo="Typo.body1" Class="py-1">Current maintainer</MudText>
<MudText Typo="Typo.h5" Class="py-1">Current maintainer</MudText>
<MudGrid Class="mt-0 mb-4">
<MudItem xs="2">
<MudText Typo="Typo.body1">Name</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Name</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Sledgehammer999</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Nationality</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Nationality</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Greece</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">E-mail</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">E-mail</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudLink Href="mailto:sledgehammer999@qbittorrent.org">sledgehammer999@qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
<MudText Typo="Typo.body1" Class="py-1">Original author</MudText>
<MudText Typo="Typo.h5" Class="py-1">Original author</MudText>
<MudGrid Class="mt-0 mb-4">
<MudItem xs="2">
<MudText Typo="Typo.body1">Name</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Name</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">Christophe Dumez</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">Nationality</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">Nationality</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudText Typo="Typo.body1">France</MudText>
</MudItem>
<MudItem xs="2">
<MudText Typo="Typo.body1">E-mail</MudText>
<MudItem xs="12" md="2">
<MudText Typo="Typo.h6">E-mail</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="12" md="10">
<MudLink Href="mailto:chris@qbittorrent.org">chris@qbittorrent.org</MudLink>
</MudItem>
</MudGrid>
@@ -118,7 +125,7 @@
(the list might not be up to date)
</MudText>
<MudList T="string" ReadOnly>
<MudListItem Icon="@Icons.Material.Filled.Circle" IconColor="Color.Info"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Arabic:</u> SDERAWI (abz8868@msn.com), sn51234 (nesseyan@gmail.com) and Ibrahim Saed ibraheem_alex(Transifex)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Armenian:</u> Hrant Ohanyan (hrantohanyan@mail.am)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Basque:</u> Xabier Aramendi (azpidatziak@gmail.com)</MudListItem>
<MudListItem Icon="@Icons.Material.Filled.Circle"><u>Belarusian:</u> Mihas Varantsou (meequz@gmail.com)</MudListItem>
@@ -1058,38 +1065,38 @@
<MudText Typo="Typo.body1" Class="py-1">qBittorrent was built with the following libraries:</MudText>
<MudGrid Class="mt-1 mb-4">
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Qt</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@QtVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Libtorrent</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@LibtorrentVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">Boost</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@BoostVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">OpenSSL</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@OpensslVersion</MudText>
</MudItem>
<MudItem xs="2">
<MudItem xs="3">
<MudText Typo="Typo.body1">zlib</MudText>
</MudItem>
<MudItem xs="10">
<MudItem xs="9">
<MudText Typo="Typo.body1">@ZlibVersion</MudText>
</MudItem>
</MudGrid>

View File

@@ -1,6 +1,7 @@
@page "/details/{hash}"
@layout DetailsLayout
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" Dense="true">
@if (!DrawerOpen)
{
@@ -14,6 +15,7 @@
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">@Name</MudText>
</MudToolBar>
</div>
@if (ShowTabs)
{

View File

@@ -9,6 +9,8 @@
}
<MudDivider Vertical="true" />
<MudText Class="pl-5 no-wrap">Search</MudText>
<MudDivider Vertical="true" />
<MudIconButton Icon="@Icons.Material.Outlined.Settings" OnClick="ShowSearchPlugins" title="Search plugins" />
</MudToolBar>
<MudCard Elevation="1" Class="ml-4 mr-4 mb-4">
@@ -18,7 +20,7 @@
<MudItem xs="12" md="4">
<MudTextField T="string" Label="Criteria" @bind-Value="Model.SearchText" Variant="Variant.Outlined" />
</MudItem>
<MudItem xs="2" md="3">
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Categories" @bind-Value="Model.SelectedCategory" Variant="Variant.Outlined">
@foreach (var (value, name) in Categories)
{
@@ -30,17 +32,21 @@
}
</MudSelect>
</MudItem>
<MudItem xs="2" md="3">
<MudItem xs="12" md="3">
<MudSelect T="string" Label="Plugins" @bind-Value="Model.SelectedPlugin" Variant="Variant.Outlined">
<MudSelectItem Value="@("all")">All</MudSelectItem>
<MudDivider />
@if (Plugins.Count > 0)
{
<MudDivider />
}
@foreach (var (value, name) in Plugins)
{
<MudSelectItem Value="value">@name</MudSelectItem>
}
</MudSelect>
</MudItem>
<MudItem xs="2" md="2">
<MudItem xs="12" 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>
</MudItem>

View File

@@ -150,6 +150,11 @@ namespace Lantean.QBTMud.Pages
}
}
protected async Task ShowSearchPlugins()
{
await DialogService.ShowSearchPluginsDialog();
}
protected IEnumerable<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> Columns => ColumnsDefinitions;
public static List<ColumnDefinition<QBitTorrentClient.Models.SearchResult>> ColumnsDefinitions { get; } =

View File

@@ -7,6 +7,7 @@
<TorrentActions RenderType="RenderType.MenuItems" Hashes="GetContextMenuTargetHashes()" PrimaryHash="@(ContextMenuItem?.Hash)" Torrents="MainData.Torrents" Preferences="Preferences" />
</ContextMenu>
<div style="overflow-x: auto; white-space: nowrap; width: 100%;">
<MudToolBar Gutters="false" 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" />
@@ -18,6 +19,7 @@
<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>
</div>
<MudContainer MaxWidth="MaxWidth.ExtraExtraLarge" Class="ma-0 pa-0">
<DynamicTable

View File

@@ -5,7 +5,7 @@ namespace Lantean.QBTMud.Services
{
public class DataManager : IDataManager
{
private static readonly Status[] _statuses = Enum.GetValues<Status>();
private static Status[]? _statusArray = null;
public PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers)
{
@@ -25,8 +25,9 @@ namespace Lantean.QBTMud.Services
return peerList;
}
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData)
public MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version)
{
var majorVersion = VersionHelper.GetMajorVersion(version);
var torrents = new Dictionary<string, Torrent>(mainData.Torrents?.Count ?? 0);
if (mainData.Torrents is not null)
{
@@ -87,8 +88,9 @@ namespace Lantean.QBTMud.Services
categoriesState.Add(category, torrents.Values.Where(t => FilterHelper.FilterCategory(t, category, serverState.UseSubcategories)).ToHashesHashSet());
}
var statusState = new Dictionary<string, HashSet<string>>(_statuses.Length + 2);
foreach (var status in _statuses)
var statuses = GetStatuses(majorVersion).ToArray();
var statusState = new Dictionary<string, HashSet<string>>(statuses.Length + 2);
foreach (var status in statuses)
{
statusState.Add(status.ToString(), torrents.Values.Where(t => FilterHelper.FilterStatus(t, status)).ToHashesHashSet());
}
@@ -101,7 +103,7 @@ namespace Lantean.QBTMud.Services
trackersState.Add(tracker, torrents.Values.Where(t => FilterHelper.FilterTracker(t, tracker)).ToHashesHashSet());
}
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState);
var torrentList = new MainData(torrents, tags, categories, trackers, serverState, tagState, categoriesState, statusState, trackersState, majorVersion);
return torrentList;
}
@@ -206,7 +208,7 @@ namespace Lantean.QBTMud.Services
{
foreach (var (url, hashes) in mainData.Trackers)
{
if (!torrentList.Trackers.TryGetValue(url, out var existingHashes))
if (!torrentList.Trackers.TryGetValue(url, out _))
{
torrentList.Trackers.Add(url, hashes);
}
@@ -225,7 +227,7 @@ namespace Lantean.QBTMud.Services
{
var newTorrent = CreateTorrent(hash, torrent);
torrentList.Torrents.Add(hash, newTorrent);
AddTorrentToStates(torrentList, hash);
AddTorrentToStates(torrentList, hash, torrentList.MajorVersion);
}
else
{
@@ -241,7 +243,7 @@ namespace Lantean.QBTMud.Services
}
}
private static void AddTorrentToStates(MainData torrentList, string hash)
private static void AddTorrentToStates(MainData torrentList, string hash, int version)
{
var torrent = torrentList.Torrents[hash];
@@ -271,7 +273,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(version))
{
torrentList.StatusState[status.ToString()].AddIfTrue(hash, FilterHelper.FilterStatus(torrent, status));
}
@@ -289,6 +291,25 @@ namespace Lantean.QBTMud.Services
}
}
private static Status[] GetStatuses(int version)
{
if (_statusArray is not null)
{
return _statusArray;
}
if (version == 5)
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Paused).ToArray();
}
else
{
_statusArray = Enum.GetValues<Status>().Where(s => s != Status.Stopped).ToArray();
}
return _statusArray;
}
private static void UpdateTorrentStates(MainData torrentList, string hash)
{
var torrent = torrentList.Torrents[hash];
@@ -317,7 +338,7 @@ namespace Lantean.QBTMud.Services
value.AddIfTrueOrRemove(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(torrentList.MajorVersion))
{
torrentList.StatusState[status.ToString()].AddIfTrueOrRemove(hash, FilterHelper.FilterStatus(torrent, status));
}
@@ -361,7 +382,7 @@ namespace Lantean.QBTMud.Services
categoryState.RemoveIfTrue(hash, FilterHelper.FilterCategory(torrent, category, torrentList.ServerState.UseSubcategories));
}
foreach (var status in _statuses)
foreach (var status in GetStatuses(torrentList.MajorVersion))
{
if (!torrentList.StatusState.TryGetValue(status.ToString(), out var statusState))
{

View File

@@ -4,7 +4,7 @@ namespace Lantean.QBTMud.Services
{
public interface IDataManager
{
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData);
MainData CreateMainData(QBitTorrentClient.Models.MainData mainData, string version);
Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent);

View File

@@ -155,7 +155,7 @@ code {
}
.torrent-list .mud-table-container {
height: calc(100vh - 149px);
height: calc(100vh - 160px);
}
.file-list .mud-table-container {
@@ -251,4 +251,8 @@ td .folder-button {
width: 25px;
max-width: 25px;
padding: 0 8px !important;
}
.mud-popover .mud-divider:last-child {
display: none;
}

View File

@@ -9,12 +9,12 @@
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Nunito+Sans:ital,opsz,wght@0,6..12,200..1000;1,6..12,200..1000&display=swap" rel="stylesheet">
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="css/app.css" />
<link href="./_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
<link rel="stylesheet" href="./css/app.css" />
<link rel="icon" type="image/png" href="images/qbittorrent32.png" />
<link rel="icon" href="images/qbittorrent-tray.svg">
<link rel="mask-icon" href="images/qbittorrent-tray.svg" color="#000000">
<link rel="apple-touch-icon" href="images/qbittorrent32.png">
<link rel="icon" href="./images/qbittorrent-tray.svg">
<link rel="mask-icon" href="./images/qbittorrent-tray.svg" color="#000000">
<link rel="apple-touch-icon" href="./images/qbittorrent32.png">
</head>
<body>
@@ -31,10 +31,10 @@
<a href="" class="reload">Reload</a>
<a class="dismiss">🗙</a>
</div>
<script src="_framework/blazor.webassembly.js"></script>
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
<script src="js/piecesbar.js"></script>
<script src="js/interop.js"></script>
<script src="./_framework/blazor.webassembly.js"></script>
<script src="./_content/MudBlazor/MudBlazor.min.js"></script>
<script src="./js/piecesbar.js"></script>
<script src="./js/interop.js"></script>
</body>
</html>

View File

@@ -4,7 +4,7 @@
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
</Project>

View File

@@ -1,14 +1,82 @@
# qbt-mud
# qbtmud
## To-Do
qbtmud is a drop-in replacement for qBittorrent's default WebUI, implementing all of its functionality with a modern and user-friendly interface.
- Rename multiple files dialog
- ~~RSS feeds and dialogs~~
- ~~About~~
- ~~Context menu for files list/trackers list/peers list~~
- ~~Tag management page~~
- ~~Category management page~~
- ~~Update all tables to use DynamicTable~~
- ~~Log~~
- ~~Blocks~~
- ~~Search~~
## Features
qbtmud replicates all core features of the qBittorrent WebUI, including:
- **Torrent Management** Add, remove, and control torrents.
- **Tracker Control** View and manage trackers.
- **Peer Management** Monitor and manage peers connected to torrents.
- **File Prioritization** Select and prioritize specific files within a torrent.
- **Speed Limits** Set global and per-torrent speed limits.
- **RSS Integration** Subscribe to RSS feeds for automated torrent downloads.
- **Search Functionality** Integrated torrent search.
- **Sequential Downloading** Download files in order for media streaming.
- **Super Seeding Mode** Efficiently distribute torrents as an initial seeder.
- **IP Filtering** Improve security by filtering specific IP addresses.
- **IPv6 Support** Full support for IPv6 networks.
- **Bandwidth Scheduler** Schedule bandwidth limits.
- **WebUI Access** Remotely manage torrents through the WebUI.
![image](https://github.com/user-attachments/assets/c4e383fd-bff0-4367-b6de-79e19a632f11)
![image](https://github.com/user-attachments/assets/4ff56ed6-cc11-42cd-a070-23f086fd8821)
![image](https://github.com/user-attachments/assets/e321c5a2-ccf1-4205-828d-7ed7adade7dd)
For a detailed explanation of these features, refer to the [qBittorrent Options Guide](https://github.com/qbittorrent/qBittorrent/wiki/Explanation-of-Options-in-qBittorrent).
---
## Installation
To install qbtmud without building from source:
### 1. Download the Latest Release
- Go to the [qbtmud Releases](https://github.com/lantean-code/qbtmud/releases) page.
- Download the latest release archive for your operating system.
### 2. Extract the Archive
- Extract the contents of the downloaded archive to a directory of your choice.
### 3. Configure qBittorrent to Use qbtmud
- Open qBittorrent and navigate to `Tools` > `Options` > `Web UI`.
- Enable the option **"Use alternative WebUI"**.
- Set the **"Root Folder"** to the directory where you extracted qbtmud.
- Click **OK** to save the settings.
### 4. Access qbtmud
- Open your web browser and go to `http://localhost:8080` (or the port configured in qBittorrent).
For more detailed instructions, refer to the [Alternate WebUI Usage Guide](https://github.com/qbittorrent/qBittorrent/wiki/Alternate-WebUI-usage).
---
## Building from Source
To build qbtmud from source, you need to have the **.NET 9.0 SDK** installed on your system.
### 1. Clone the Repository
```sh
git clone https://github.com/lantean-code/qbtmud.git
cd qbtmud
```
### 2. Restore Dependencies
```sh
dotnet restore
```
### 3. Build the Application
```sh
dotnet build --configuration Release
```
### 4. Configure qBittorrent to Use qbtmud
Follow the same steps as in the **Installation** section to set qbtmud as your WebUI.
### 5. Run qbtmud
Navigate to the directory containing the built files and run the application using the appropriate command for your OS.
By following these steps, you can set up qbtmud to manage your qBittorrent server with an improved web interface, offering better functionality and usability.