├── src ├── Lantean.QBTMud │ ├── Components │ │ ├── UI │ │ │ ├── NonRendering.razor │ │ │ ├── TdExtended.razor │ │ │ ├── FieldSwitch.razor │ │ │ ├── TickSwitch.razor │ │ │ ├── CellMouseEventArgs.cs │ │ │ ├── CellLongPressEventArgs.cs │ │ │ ├── NonRendering.razor.cs │ │ │ ├── CustomNavLink.razor │ │ │ ├── SortLabel.razor │ │ │ ├── BreakpointOrientationProvider.razor │ │ │ ├── BreakpointOrientationProviderCascades.cs │ │ │ ├── TdExtended.razor.cs │ │ │ ├── TableDataLongPressEventArgs.cs │ │ │ ├── TableDataContextMenuEventArgs.cs │ │ │ └── FieldSwitch.razor.cs │ │ ├── PieceProgress.razor │ │ ├── StatusBar.razor │ │ ├── StatusBar.razor.cs │ │ ├── Menu.razor │ │ ├── Dialogs │ │ │ ├── ConfirmDialog.razor │ │ │ ├── TorrentOptionsDialog.razor │ │ │ ├── ExceptionDialog.razor.cs │ │ │ ├── SubMenuDialog.razor │ │ │ ├── StringFieldDialog.razor │ │ │ ├── NumericFieldDialog.razor │ │ │ ├── DeleteDialog.razor │ │ │ ├── CategoryPropertiesDialog.razor │ │ │ ├── AddTorrentLinkDialog.razor │ │ │ ├── DeleteDialog.razor.cs │ │ │ ├── ManageTagsDialog.razor │ │ │ ├── ManageCategoriesDialog.razor │ │ │ ├── ConfirmDialog.razor.cs │ │ │ ├── AddTagDialog.razor │ │ │ ├── StringFieldDialog.razor.cs │ │ │ ├── MultipleFieldDialog.razor │ │ │ ├── AddTrackerDialog.razor │ │ │ ├── ExceptionDialog.razor │ │ │ ├── SubMenuDialog.razor.cs │ │ │ ├── AddPeerDialog.razor │ │ │ ├── SliderFieldDialog.razor │ │ │ ├── AddTrackerDialog.razor.cs │ │ │ ├── AddTorrentFileDialog.razor.cs │ │ │ ├── AddTorrentLinkDialog.razor.cs │ │ │ ├── SubmittableDialog.cs │ │ │ ├── AddPeerDialog.razor.cs │ │ │ ├── CategoryPropertiesDialog.razor.cs │ │ │ ├── AddTagDialog.razor.cs │ │ │ ├── TorrentOptionsDialog.razor.cs │ │ │ ├── MultipleFieldDialog.razor.cs │ │ │ ├── NumericFieldDialog.razor.cs │ │ │ └── ColumnOptionsDialog.razor │ │ ├── ErrorDisplay.razor │ │ ├── WebSeedsTab.razor │ │ ├── TorrentsListNav.razor │ │ ├── TorrentInfo.razor.cs │ │ ├── Menu.razor.cs │ │ ├── TorrentInfo.razor │ │ ├── ErrorDisplay.razor.cs │ │ ├── TorrentsListNav.razor.cs │ │ ├── EnhancedErrorBoundary.cs │ │ ├── PiecesProgressCanvas.razor │ │ ├── PeersTab.razor │ │ └── Options │ │ │ └── Options.cs │ ├── wwwroot │ │ ├── js │ │ │ └── boot.settings.js │ │ ├── favicon.png │ │ ├── icon-192.png │ │ ├── images │ │ │ ├── mascot.png │ │ │ ├── qbittorrent32.png │ │ │ └── qbittorrent-tray.svg │ │ ├── Lantean.QBTMud.lib.module.js │ │ └── index.html │ ├── Models │ │ ├── ContentItemType.cs │ │ ├── TorrentFilterField.cs │ │ ├── Priority.cs │ │ ├── SpeedPoint.cs │ │ ├── AppliesTo.cs │ │ ├── PeerList.cs │ │ ├── SpeedDirection.cs │ │ ├── Category.cs │ │ ├── RssTreeItem.cs │ │ ├── Status.cs │ │ ├── SearchJobMetadata.cs │ │ ├── SpeedPeriod.cs │ │ ├── LoginForm.cs │ │ ├── AddTorrentLinkOptions.cs │ │ ├── AddTorrentFileOptions.cs │ │ ├── FilterSearchState.cs │ │ ├── RowContext.cs │ │ ├── ShareRatio.cs │ │ ├── SearchFilterOptions.cs │ │ ├── LogForm.cs │ │ ├── FileRow.cs │ │ ├── SearchPreferences.cs │ │ ├── RssTreeNode.cs │ │ ├── SearchForm.cs │ │ ├── RssFeed.cs │ │ ├── FilterState.cs │ │ ├── RssArticle.cs │ │ ├── GlobalTransferInfo.cs │ │ ├── MainData.cs │ │ ├── ColumnDefinition.cs │ │ └── ContentItem.cs │ ├── Services │ │ ├── IClipboardService.cs │ │ ├── IRssDataManager.cs │ │ ├── IPreferencesDataManager.cs │ │ ├── IPeerDataManager.cs │ │ ├── IKeyboardService.cs │ │ ├── CookieHandler.cs │ │ ├── IPeriodicTimerFactory.cs │ │ ├── ITorrentDataManager.cs │ │ ├── IPeriodicTimer.cs │ │ ├── ClipboardService.cs │ │ ├── PeriodicTimerFactory.cs │ │ └── RssDataManager.cs │ ├── Interop │ │ ├── ClientSize.cs │ │ ├── MagnetRegistrationResult.cs │ │ └── BoundingClientRect.cs │ ├── GlobalSuppressions.cs │ ├── EventHandlers │ │ └── EventHandlers.cs │ ├── App.razor │ ├── Helpers │ │ ├── TestIdHelper.cs │ │ ├── EventArgsExtensions.cs │ │ └── ColumnDefinitionHelper.cs │ ├── Layout │ │ ├── OtherLayout.razor │ │ ├── DetailsLayout.razor │ │ ├── ListLayout.razor │ │ ├── OtherLayout.razor.cs │ │ ├── ListLayout.razor.cs │ │ ├── DetailsLayout.razor.cs │ │ └── MainLayout.razor │ ├── _Imports.razor │ ├── Properties │ │ └── launchSettings.json │ ├── Filter │ │ └── PropertyFilterDefinition.cs │ ├── CustomIcons.cs │ └── Pages │ │ ├── Statistics.razor.cs │ │ ├── Login.razor.cs │ │ ├── Tags.razor │ │ ├── About.razor.cs │ │ ├── Rss.razor.css │ │ ├── Categories.razor │ │ ├── Login.razor │ │ └── Details.razor └── Lantean.QBitTorrentClient │ ├── Limits.cs │ ├── Models │ ├── PieceState.cs │ ├── StopCondition.cs │ ├── TorrentContentLayout.cs │ ├── DirectoryContentMode.cs │ ├── LogType.cs │ ├── Priority.cs │ ├── ShareLimitAction.cs │ ├── TrackerStatus.cs │ ├── PeerId.cs │ ├── WebSeed.cs │ ├── DownloadPathOption.cs │ ├── SearchCategory.cs │ ├── NetworkInterface.cs │ ├── SearchStatus.cs │ ├── SearchResults.cs │ ├── SslParameters.cs │ ├── Log.cs │ ├── Category.cs │ ├── ApplicationCookie.cs │ ├── PeerLog.cs │ ├── TorrentPeers.cs │ ├── BuildInfo.cs │ ├── SearchPlugin.cs │ ├── AddTorrentResult.cs │ ├── RssItem.cs │ ├── FileData.cs │ ├── SearchResult.cs │ ├── RssArticle.cs │ ├── TorrentParams.cs │ ├── AddTorrentParams.cs │ └── GlobalTransferInfo.cs │ ├── Lantean.QBitTorrentClient.csproj │ ├── HttpClientExtensions.cs │ ├── SerializerOptions.cs │ ├── Converters │ ├── StringFloatJsonConverter.cs │ ├── CommaSeparatedJsonConverter.cs │ ├── SaveLocationJsonConverter.cs │ ├── NullableStringFloatJsonConverter.cs │ └── DownloadPathOptionJsonConverter.cs │ ├── MultipartFormDataContentExtensions.cs │ ├── FormUrlEncodedBuilder.cs │ └── QueryBuilderExtensions.cs ├── global.json ├── .github └── dependabot.yml ├── test ├── Lantean.QBitTorrentClient.Test │ ├── StubHttpMessageHandler.cs │ ├── Lantean.QBitTorrentClient.Test.csproj │ └── Converters │ │ └── NullableStringFloatJsonConverterTests.cs └── Lantean.QBTMud.Test │ ├── Components │ ├── UI │ │ ├── NonRenderingTests.cs │ │ ├── TickSwitchTests.cs │ │ ├── TdExtendedTests.cs │ │ └── FieldSwitchTests.cs │ └── MenuTests.cs │ ├── Infrastructure │ ├── TestClipboardService.cs │ ├── RazorComponentTestBase.cs │ └── ComponentTestContextTests.cs │ ├── Services │ ├── PeriodicTimerFactoryTests.cs │ └── CookieHandlerTests.cs │ ├── Lantean.QBTMud.Test.csproj │ ├── Models │ └── SearchJobViewModelTests.cs │ └── Helpers │ └── SearchFilterHelperTests.cs └── .dcignore /src/Lantean.QBTMud/Components/UI/NonRendering.razor: -------------------------------------------------------------------------------- 1 | @ChildContent -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/PieceProgress.razor: -------------------------------------------------------------------------------- 1 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/StatusBar.razor: -------------------------------------------------------------------------------- 1 |

StatusBar

2 | 3 | @code { 4 | 5 | } 6 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/js/boot.settings.js: -------------------------------------------------------------------------------- 1 | window.__useCdnAot = false; 2 | window.__cdnBase = ""; 3 | -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "sdk": { 3 | "version": "10.0.100", 4 | "rollForward": "latestFeature" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lantean-code/qbtmud/HEAD/src/Lantean.QBTMud/wwwroot/favicon.png -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lantean-code/qbtmud/HEAD/src/Lantean.QBTMud/wwwroot/icon-192.png -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/images/mascot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lantean-code/qbtmud/HEAD/src/Lantean.QBTMud/wwwroot/images/mascot.png -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/images/qbittorrent32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lantean-code/qbtmud/HEAD/src/Lantean.QBTMud/wwwroot/images/qbittorrent32.png -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/StatusBar.razor.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Components 2 | { 3 | public partial class StatusBar 4 | { 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/ContentItemType.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public enum ContentItemType 4 | { 5 | File, 6 | Folder 7 | } 8 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/TorrentFilterField.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public enum TorrentFilterField 4 | { 5 | Name = 0, 6 | SavePath = 1 7 | } 8 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IClipboardService.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Services 2 | { 3 | public interface IClipboardService 4 | { 5 | Task WriteToClipboard(string text); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Limits.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient 2 | { 3 | public static class Limits 4 | { 5 | public const int GlobalLimit = -2; 6 | 7 | public const int NoLimit = -1; 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/PieceState.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum PieceState 4 | { 5 | NotDownloaded = 0, 6 | Downloading = 1, 7 | Downloaded = 2, 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/StopCondition.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum StopCondition 4 | { 5 | None = 0, 6 | MetadataReceived = 1, 7 | FilesChecked = 2 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/TorrentContentLayout.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum TorrentContentLayout 4 | { 5 | Original, 6 | Subfolder, 7 | NoSubfolder 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Interop/ClientSize.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Interop 2 | { 3 | public class ClientSize 4 | { 5 | public double Width { get; set; } 6 | 7 | public double Height { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/DirectoryContentMode.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum DirectoryContentMode 4 | { 5 | All, 6 | Directories, 7 | Files 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/LogType.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum LogType 4 | { 5 | Normal = 1, 6 | Info = 2, 7 | Warning = 4, 8 | Critical = 8 9 | } 10 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/Priority.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum Priority 4 | { 5 | DoNotDownload = 0, 6 | Normal = 1, 7 | High = 6, 8 | Maximum = 7 9 | } 10 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/Priority.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public enum Priority 4 | { 5 | Mixed = -1, 6 | DoNotDownload = 0, 7 | Normal = 1, 8 | High = 6, 9 | Maximum = 7 10 | } 11 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SpeedPoint.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | /// 4 | /// Represents a single data point on a speed chart. 5 | /// 6 | public record SpeedPoint(DateTime TimestampUtc, double BytesPerSecond); 7 | } 8 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Interop/MagnetRegistrationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Interop 2 | { 3 | public class MagnetRegistrationResult 4 | { 5 | public string? Status { get; set; } 6 | 7 | public string? Message { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/GlobalSuppressions.cs: -------------------------------------------------------------------------------- 1 | // This file is used by Code Analysis to maintain SuppressMessage 2 | // attributes that are applied to this project. 3 | // Project-level suppressions either have no target or are given 4 | // a specific target and scoped to a namespace, type, member, etc. -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IRssDataManager.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public interface IRssDataManager 6 | { 7 | RssList CreateRssList(IReadOnlyDictionary rssItems); 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/AppliesTo.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel; 2 | 3 | namespace Lantean.QBTMud.Models 4 | { 5 | public enum AppliesTo 6 | { 7 | [Description("Filename + Extension")] 8 | FilenameExtension, 9 | 10 | Filename, 11 | Extension 12 | } 13 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/EventHandlers/EventHandlers.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Lantean.QBTMud 4 | { 5 | [EventHandler("onlongpress", typeof(LongPressEventArgs), enableStopPropagation: true, enablePreventDefault: true)] 6 | public static class EventHandlers 7 | { 8 | } 9 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/PeerList.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record PeerList 4 | { 5 | public PeerList(Dictionary peers) 6 | { 7 | Peers = peers; 8 | } 9 | 10 | public Dictionary Peers { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SpeedDirection.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | /// 4 | /// Indicates whether a speed series represents download or upload. 5 | /// 6 | public enum SpeedDirection 7 | { 8 | Download = 0, 9 | Upload 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/TdExtended.razor: -------------------------------------------------------------------------------- 1 | @inherits MudTd 2 | 3 | 4 | @ChildContent 5 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/ShareLimitAction.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum ShareLimitAction 4 | { 5 | Default = -1, // special value 6 | 7 | Stop = 0, 8 | Remove = 1, 9 | RemoveWithContent = 3, 10 | EnableSuperSeeding = 2 11 | } 12 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IPreferencesDataManager.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Services 2 | { 3 | public interface IPreferencesDataManager 4 | { 5 | QBitTorrentClient.Models.UpdatePreferences MergePreferences(QBitTorrentClient.Models.UpdatePreferences? original, QBitTorrentClient.Models.UpdatePreferences changed); 6 | } 7 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/TrackerStatus.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public enum TrackerStatus 4 | { 5 | Disabled = 0, 6 | Uncontacted = 1, 7 | Working = 2, 8 | Updating = 3, 9 | NotWorking = 4, 10 | Error = 5, 11 | Unreachable = 6 12 | } 13 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Menu.razor: -------------------------------------------------------------------------------- 1 | @if (_isVisible) 2 | { 3 | 4 | 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/FieldSwitch.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/Category.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record Category 4 | { 5 | public Category(string name, string savePath) 6 | { 7 | Name = name; 8 | SavePath = savePath; 9 | } 10 | 11 | public string Name { get; set; } 12 | public string SavePath { get; set; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ConfirmDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | @Content 6 | 7 | 8 | @CancelText 9 | @SuccessText 10 | 11 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/RssTreeItem.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public sealed class RssTreeItem 4 | { 5 | public RssTreeItem(RssTreeNode node, int depth) 6 | { 7 | Node = node; 8 | Depth = depth; 9 | } 10 | 11 | public RssTreeNode Node { get; } 12 | 13 | public int Depth { get; } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IPeerDataManager.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public interface IPeerDataManager 6 | { 7 | PeerList CreatePeerList(QBitTorrentClient.Models.TorrentPeers torrentPeers); 8 | 9 | void MergeTorrentPeers(QBitTorrentClient.Models.TorrentPeers torrentPeers, PeerList peerList); 10 | } 11 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/PeerId.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public readonly struct PeerId(string host, int port) 4 | { 5 | public string Host { get; } = host; 6 | 7 | public int Port { get; } = port; 8 | 9 | public override string ToString() 10 | { 11 | return $"{Host}:{Port}"; 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/Status.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public enum Status 4 | { 5 | All, 6 | Downloading, 7 | Seeding, 8 | Completed, 9 | Stopped, 10 | Active, 11 | Inactive, 12 | Stalled, 13 | StalledUploading, 14 | StalledDownloading, 15 | Checking, 16 | Errored, 17 | } 18 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/ErrorDisplay.razor: -------------------------------------------------------------------------------- 1 | 2 | Clear Errors 3 | Clear Errors and Resume 4 | 5 | @foreach (var error in Errors) 6 | { 7 | @error.Message 8 | } 9 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SearchJobMetadata.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class SearchJobMetadata 4 | { 5 | public int Id { get; set; } 6 | 7 | public string Pattern { get; set; } = string.Empty; 8 | 9 | public string Category { get; set; } = SearchForm.AllCategoryId; 10 | 11 | public List Plugins { get; set; } = []; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SpeedPeriod.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | /// 4 | /// Represents the duration of time to render on the transfer speed chart. 5 | /// 6 | public enum SpeedPeriod 7 | { 8 | Min1 = 0, 9 | Min5, 10 | Min30, 11 | Hour3, 12 | Hour6, 13 | Hour12, 14 | Hour24 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/WebSeed.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record WebSeed 6 | { 7 | [JsonConstructor] 8 | public WebSeed(string url) 9 | { 10 | Url = url; 11 | } 12 | 13 | [JsonPropertyName("url")] 14 | public string Url { get; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/DownloadPathOption.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public record DownloadPathOption 4 | { 5 | public DownloadPathOption(bool enabled, string? path) 6 | { 7 | Enabled = enabled; 8 | Path = path; 9 | } 10 | 11 | public bool Enabled { get; } 12 | 13 | public string? Path { get; } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/LoginForm.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | using System.Diagnostics.CodeAnalysis; 3 | 4 | namespace Lantean.QBTMud.Models 5 | { 6 | public class LoginForm 7 | { 8 | [Required] 9 | [NotNull] 10 | public string? Username { get; set; } 11 | 12 | [Required] 13 | [NotNull] 14 | public string? Password { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IKeyboardService.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public interface IKeyboardService 6 | { 7 | Task Focus(); 8 | 9 | Task UnFocus(); 10 | 11 | Task RegisterKeypressEvent(KeyboardEvent criteria, Func onKeyPress); 12 | 13 | Task UnregisterKeypressEvent(KeyboardEvent criteria); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/TorrentOptionsDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | Cancel 11 | Save 12 | 13 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Interop/BoundingClientRect.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Interop 2 | { 3 | public class BoundingClientRect : ClientSize 4 | { 5 | public double Bottom { get; set; } 6 | 7 | public double Top { get; set; } 8 | 9 | public double Left { get; set; } 10 | 11 | public double Right { get; set; } 12 | 13 | public double X { get; set; } 14 | 15 | public double Y { get; set; } 16 | } 17 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/AddTorrentLinkOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record AddTorrentLinkOptions : TorrentOptions 4 | { 5 | public AddTorrentLinkOptions(string urls, TorrentOptions options) : base(options) 6 | { 7 | Urls = urls.Split('\n', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries); 8 | } 9 | 10 | public IReadOnlyList Urls { get; } 11 | } 12 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/AddTorrentFileOptions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Forms; 2 | 3 | namespace Lantean.QBTMud.Models 4 | { 5 | public record AddTorrentFileOptions : TorrentOptions 6 | { 7 | public AddTorrentFileOptions(IReadOnlyList files, TorrentOptions options) : base(options) 8 | { 9 | Files = files; 10 | } 11 | 12 | public IReadOnlyList Files { get; } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Lantean.QBitTorrentClient.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | true 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/TickSwitch.razor: -------------------------------------------------------------------------------- 1 | @inherits MudSwitch 2 | @typeparam T 3 | @{ 4 | base.BuildRenderTree(__builder); 5 | } 6 | 7 | @code{ 8 | protected override void OnParametersSet() 9 | { 10 | if (Value is bool boolValue) 11 | { 12 | this.ThumbIcon = boolValue ? Icons.Material.Filled.Done : Icons.Material.Filled.Close; 13 | this.ThumbIconColor = boolValue ? Color.Success : Color.Error; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/WebSeedsTab.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/App.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Not found 8 | 9 |

Sorry, there's nothing at this address.

10 |
11 |
12 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/CellMouseEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Web; 2 | 3 | namespace Lantean.QBTMud.Components.UI 4 | { 5 | public sealed class CellMouseEventArgs 6 | { 7 | public CellMouseEventArgs(MouseEventArgs mouseEventArgs, TdExtended cell) 8 | { 9 | MouseEventArgs = mouseEventArgs; 10 | Cell = cell; 11 | } 12 | 13 | public MouseEventArgs MouseEventArgs { get; } 14 | 15 | public TdExtended Cell { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/CellLongPressEventArgs.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace Lantean.QBTMud.Components.UI 4 | { 5 | public sealed class CellLongPressEventArgs 6 | { 7 | public CellLongPressEventArgs(LongPressEventArgs longPressEventArgs, TdExtended cell) 8 | { 9 | LongPressEventArgs = longPressEventArgs; 10 | Cell = cell; 11 | } 12 | 13 | public LongPressEventArgs LongPressEventArgs { get; } 14 | 15 | public TdExtended Cell { get; } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Helpers/TestIdHelper.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Helpers 2 | { 3 | public static class TestIdHelper 4 | { 5 | private static bool _useTestIds = false; 6 | 7 | public static string? For(string id) 8 | { 9 | if (_useTestIds) 10 | { 11 | return id; 12 | } 13 | 14 | return null; 15 | } 16 | 17 | internal static void EnableTestIds() 18 | { 19 | _useTestIds = true; 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ExceptionDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace Lantean.QBTMud.Components.Dialogs 5 | { 6 | public partial class ExceptionDialog 7 | { 8 | [CascadingParameter] 9 | private IMudDialogInstance MudDialog { get; set; } = default!; 10 | 11 | [Parameter] 12 | public Exception? Exception { get; set; } 13 | 14 | protected void Close() 15 | { 16 | MudDialog.Cancel(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/NonRendering.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Lantean.QBTMud.Components.UI 4 | { 5 | /// 6 | /// A simple razor wrapper that only renders the child content without any additonal html markup 7 | /// 8 | public partial class NonRendering 9 | { 10 | /// 11 | /// The child content to be rendered 12 | /// 13 | [Parameter] 14 | public RenderFragment? ChildContent { get; set; } 15 | } 16 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/CookieHandler.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.WebAssembly.Http; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public class CookieHandler : DelegatingHandler 6 | { 7 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 8 | { 9 | request.SetBrowserRequestCredentials(BrowserRequestCredentials.Include); 10 | 11 | return base.SendAsync(request, cancellationToken); 12 | } 13 | } 14 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/OtherLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout LoggedInLayout 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | 11 | @Body 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SearchCategory.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SearchCategory 6 | { 7 | [JsonConstructor] 8 | public SearchCategory(string id, string name) 9 | { 10 | Id = id; 11 | Name = name; 12 | } 13 | 14 | [JsonPropertyName("id")] 15 | public string Id { get; set; } 16 | 17 | [JsonPropertyName("name")] 18 | public string Name { get; set; } 19 | } 20 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/DetailsLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout LoggedInLayout 3 | 4 |
5 | 6 | 7 | 8 | 9 | @Body 10 | 11 |
12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/SubMenuDialog.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/NetworkInterface.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record NetworkInterface 6 | { 7 | [JsonConstructor] 8 | public NetworkInterface( 9 | string name, 10 | string value) 11 | { 12 | Name = name; 13 | Value = value; 14 | } 15 | 16 | [JsonPropertyName("name")] 17 | public string Name { get; } 18 | 19 | [JsonPropertyName("value")] 20 | public string Value { get; } 21 | } 22 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/CustomNavLink.razor: -------------------------------------------------------------------------------- 1 |
2 |
3 | @if (!string.IsNullOrEmpty(Icon)) 4 | { 5 | 6 | } 7 | 10 |
11 |
12 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IPeriodicTimerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | /// 6 | /// Creates instances of . 7 | /// 8 | public interface IPeriodicTimerFactory 9 | { 10 | /// 11 | /// Creates a timer that ticks at the specified interval. 12 | /// 13 | /// The interval between ticks. 14 | /// A new instance. 15 | IPeriodicTimer Create(TimeSpan period); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /test/Lantean.QBitTorrentClient.Test/StubHttpMessageHandler.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | 3 | namespace Lantean.QBitTorrentClient.Test 4 | { 5 | internal sealed class StubHttpMessageHandler : HttpMessageHandler 6 | { 7 | public Func>? Responder { get; set; } 8 | 9 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 10 | { 11 | Responder.Should().NotBeNull(); 12 | return Responder!(request, cancellationToken); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/StringFieldDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | Cancel 13 | Save 14 | 15 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/TorrentsListNav.razor: -------------------------------------------------------------------------------- 1 | 2 | Back 3 | 4 | @if (OrderedTorrents is null) 5 | { 6 | @for (var i = 0; i < 10; i++) 7 | { 8 | 9 | } 10 | } 11 | else 12 | { 13 | foreach (var torrent in OrderedTorrents) 14 | { 15 | @torrent.Name 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/FilterSearchState.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public readonly struct FilterSearchState 4 | { 5 | public FilterSearchState(string? text, TorrentFilterField field, bool useRegex, bool isRegexValid) 6 | { 7 | Text = text; 8 | Field = field; 9 | UseRegex = useRegex; 10 | IsRegexValid = isRegexValid; 11 | } 12 | 13 | public string? Text { get; } 14 | 15 | public TorrentFilterField Field { get; } 16 | 17 | public bool UseRegex { get; } 18 | 19 | public bool IsRegexValid { get; } 20 | } 21 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/RowContext.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record RowContext 4 | { 5 | private readonly Func _valueGetter; 6 | 7 | public RowContext(string headerText, T data, Func valueGetter) 8 | { 9 | HeaderText = headerText; 10 | Data = data; 11 | _valueGetter = valueGetter; 12 | } 13 | 14 | public string HeaderText { get; } 15 | 16 | public T Data { get; set; } 17 | 18 | public object? GetValue() 19 | { 20 | return _valueGetter(Data); 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/ShareRatio.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | 3 | namespace Lantean.QBTMud.Models 4 | { 5 | public record ShareRatio 6 | { 7 | public float RatioLimit { get; set; } 8 | public float SeedingTimeLimit { get; set; } 9 | public float InactiveSeedingTimeLimit { get; set; } 10 | public ShareLimitAction? ShareLimitAction { get; set; } 11 | } 12 | 13 | public record ShareRatioMax : ShareRatio 14 | { 15 | public float MaxRatio { get; set; } 16 | public float MaxSeedingTime { get; set; } 17 | public float MaxInactiveSeedingTime { get; set; } 18 | } 19 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/SortLabel.razor: -------------------------------------------------------------------------------- 1 | @inherits MudComponentBase 2 | 3 | 4 | @if (!AppendIcon) 5 | { 6 | @ChildContent 7 | } 8 | @if (Enabled) 9 | { 10 | @if (SortDirection != SortDirection.None) 11 | { 12 | 13 | } 14 | else 15 | { 16 | 17 | } 18 | } 19 | @if (AppendIcon) 20 | { 21 | @ChildContent 22 | } 23 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SearchStatus.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SearchStatus 6 | { 7 | [JsonConstructor] 8 | public SearchStatus(int id, string status, int total) 9 | { 10 | Id = id; 11 | Status = status; 12 | Total = total; 13 | } 14 | 15 | [JsonPropertyName("id")] 16 | public int Id { get; } 17 | 18 | [JsonPropertyName("status")] 19 | public string Status { get; } 20 | 21 | [JsonPropertyName("total")] 22 | public int Total { get; } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/HttpClientExtensions.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient 2 | { 3 | internal static class HttpClientExtensions 4 | { 5 | public static Task PostAsync(this HttpClient httpClient, string requestUrl, FormUrlEncodedBuilder builder) 6 | { 7 | return httpClient.PostAsync(requestUrl, builder.ToFormUrlEncodedContent()); 8 | } 9 | 10 | public static Task GetAsync(this HttpClient httpClient, string requestUrl, QueryBuilder builder) 11 | { 12 | return httpClient.GetAsync($"{requestUrl}{builder.ToQueryString()}"); 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/ListLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | @layout LoggedInLayout 3 | 4 |
5 | 6 | 7 | 8 | 9 | 10 | @Body 11 | 12 | 13 |
14 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/BreakpointOrientationProvider.razor: -------------------------------------------------------------------------------- 1 | @inherits MudComponentBase 2 | 3 | @if (ProvideBreakpoint && ProvideOrientation) 4 | { 5 | 6 | 7 | @ChildContent 8 | 9 | 10 | } 11 | else if (ProvideBreakpoint) 12 | { 13 | 14 | @ChildContent 15 | 16 | } 17 | else if (ProvideOrientation) 18 | { 19 | 20 | @ChildContent 21 | 22 | } 23 | else 24 | { 25 | @ChildContent 26 | } 27 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/NumericFieldDialog.razor: -------------------------------------------------------------------------------- 1 | @typeparam T 2 | @inherits SubmittableDialog 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Cancel 14 | Save 15 | 16 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/DeleteDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | Are you sure you want to remove @Count torrent@(Count == 1 ? "": "s") from the transfer list? 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Cancel 15 | Remove 16 | 17 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/_Imports.razor: -------------------------------------------------------------------------------- 1 | @using System.Net.Http 2 | @using System.Net.Http.Json 3 | @using Microsoft.AspNetCore.Components.Forms 4 | @using Microsoft.AspNetCore.Components.Routing 5 | @using Microsoft.AspNetCore.Components.Web 6 | @using Microsoft.AspNetCore.Components.Web.Virtualization 7 | @using Microsoft.AspNetCore.Components.WebAssembly.Http 8 | @using Microsoft.JSInterop 9 | @using MudBlazor 10 | @using MudBlazor.Charts 11 | @using Lantean.QBTMud 12 | @using Lantean.QBTMud.Components 13 | @using Lantean.QBTMud.Components.Dialogs 14 | @using Lantean.QBTMud.Components.Options 15 | @using Lantean.QBTMud.Components.UI 16 | @using Lantean.QBTMud.Helpers 17 | @using Lantean.QBTMud.Layout 18 | @using Lantean.QBTMud.Models 19 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SearchResults.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SearchResults 6 | { 7 | [JsonConstructor] 8 | public SearchResults(IReadOnlyList results, string status, int total) 9 | { 10 | Results = results; 11 | Status = status; 12 | Total = total; 13 | } 14 | 15 | [JsonPropertyName("results")] 16 | public IReadOnlyList Results { get; } 17 | 18 | [JsonPropertyName("status")] 19 | public string Status { get; } 20 | 21 | [JsonPropertyName("total")] 22 | public int Total { get; } 23 | } 24 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/ITorrentDataManager.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public interface ITorrentDataManager 6 | { 7 | MainData CreateMainData(QBitTorrentClient.Models.MainData mainData); 8 | 9 | Torrent CreateTorrent(string hash, QBitTorrentClient.Models.Torrent torrent); 10 | 11 | bool MergeMainData(QBitTorrentClient.Models.MainData mainData, MainData torrentList, out bool filterChanged); 12 | 13 | Dictionary CreateContentsList(IReadOnlyList files); 14 | 15 | bool MergeContentsList(IReadOnlyList files, Dictionary contents); 16 | } 17 | } -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Components/UI/NonRenderingTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBTMud.Components.UI; 3 | using Lantean.QBTMud.Test.Infrastructure; 4 | 5 | namespace Lantean.QBTMud.Test.Components.UI 6 | { 7 | public sealed class NonRenderingTests : RazorComponentTestBase 8 | { 9 | [Fact] 10 | public void GIVEN_ChildContent_WHEN_Rendered_THEN_ShouldRenderChildContent() 11 | { 12 | var target = TestContext.Render(parameters => 13 | { 14 | parameters.Add(p => p.ChildContent, builder => builder.AddContent(0, "ChildContent")); 15 | }); 16 | 17 | target.Markup.Should().Be("ChildContent"); 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SearchFilterOptions.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public enum SearchInScope 4 | { 5 | Everywhere = 0, 6 | Names = 1 7 | } 8 | 9 | public enum SearchSizeUnit 10 | { 11 | Bytes = 0, 12 | Kibibytes = 1, 13 | Mebibytes = 2, 14 | Gibibytes = 3, 15 | Tebibytes = 4, 16 | Pebibytes = 5, 17 | Exbibytes = 6, 18 | } 19 | 20 | public record SearchFilterOptions( 21 | string? FilterText, 22 | SearchInScope SearchIn, 23 | int? MinimumSeeds, 24 | int? MaximumSeeds, 25 | double? MinimumSize, 26 | SearchSizeUnit MinimumSizeUnit, 27 | double? MaximumSize, 28 | SearchSizeUnit MaximumSizeUnit); 29 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/IPeriodicTimer.cs: -------------------------------------------------------------------------------- 1 | using System.Threading; 2 | using System.Threading.Tasks; 3 | 4 | namespace Lantean.QBTMud.Services 5 | { 6 | /// 7 | /// Represents a timer that signals at a fixed interval. 8 | /// 9 | public interface IPeriodicTimer : IAsyncDisposable 10 | { 11 | /// 12 | /// Waits asynchronously for the next tick of the timer. 13 | /// 14 | /// A token to cancel the wait. 15 | /// if the timer ticked; otherwise when the timer is disposed. 16 | Task WaitForNextTickAsync(CancellationToken cancellationToken); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/BreakpointOrientationProviderCascades.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Components.UI 2 | { 3 | /// 4 | /// Specifies which values MudBreakpointOrientationProvider will cascade. 5 | /// 6 | [Flags] 7 | public enum BreakpointOrientationProviderCascades 8 | { 9 | /// 10 | /// Cascade only the current breakpoint. 11 | /// 12 | Breakpoint = 1, 13 | 14 | /// 15 | /// Cascade only the current orientation. 16 | /// 17 | Orientation = 2, 18 | 19 | /// 20 | /// Cascade both breakpoint and orientation. 21 | /// 22 | Both = Breakpoint | Orientation 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "http": { 4 | "commandName": "Project", 5 | "launchBrowser": true, 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "dotnetRunMessages": true, 10 | "inspectUri": "{wsProtocol}://{url.hostname}:{url.port}/_framework/debug/ws-proxy?browser={browserInspectUri}", 11 | "applicationUrl": "http://localhost:5140" 12 | } 13 | }, 14 | "$schema": "http://json.schemastore.org/launchsettings.json", 15 | "iisSettings": { 16 | "windowsAuthentication": false, 17 | "anonymousAuthentication": true, 18 | "iisExpress": { 19 | "applicationUrl": "http://localhost:28406", 20 | "sslPort": 0 21 | } 22 | } 23 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/TorrentInfo.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Components 5 | { 6 | public partial class TorrentInfo 7 | { 8 | [Parameter] 9 | [EditorRequired] 10 | public string Hash { get; set; } = default!; 11 | 12 | [CascadingParameter] 13 | public MainData MainData { get; set; } = default!; 14 | 15 | protected Torrent? Torrent => GetTorrent(); 16 | 17 | private Torrent? GetTorrent() 18 | { 19 | if (Hash is null || !MainData.Torrents.TryGetValue(Hash, out var torrent)) 20 | { 21 | return null; 22 | } 23 | 24 | return torrent; 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/CategoryPropertiesDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Cancel 16 | Add 17 | 18 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SslParameters.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SslParameters 6 | { 7 | [JsonConstructor] 8 | public SslParameters(string? certificate, string? privateKey, string? dhParams) 9 | { 10 | Certificate = certificate; 11 | PrivateKey = privateKey; 12 | DhParams = dhParams; 13 | } 14 | 15 | [JsonPropertyName("ssl_certificate")] 16 | public string? Certificate { get; } 17 | 18 | [JsonPropertyName("ssl_private_key")] 19 | public string? PrivateKey { get; } 20 | 21 | [JsonPropertyName("ssl_dh_params")] 22 | public string? DhParams { get; } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/SerializerOptions.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Converters; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Lantean.QBitTorrentClient 6 | { 7 | public static class SerializerOptions 8 | { 9 | public static JsonSerializerOptions Options { get; } 10 | 11 | static SerializerOptions() 12 | { 13 | Options = new JsonSerializerOptions(); 14 | Options.Converters.Add(new StringFloatJsonConverter()); 15 | Options.Converters.Add(new NullableStringFloatJsonConverter()); 16 | Options.Converters.Add(new SaveLocationJsonConverter()); 17 | Options.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTorrentLinkDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | Close 16 | Upload Torrents 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/ClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.JSInterop; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public class ClipboardService : IClipboardService 6 | { 7 | private readonly IJSRuntime _jSRuntime; 8 | 9 | public ClipboardService(IJSRuntime jSRuntime) 10 | { 11 | _jSRuntime = jSRuntime; 12 | } 13 | 14 | public async Task WriteToClipboard(string text) 15 | { 16 | try 17 | { 18 | await _jSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", text); 19 | } 20 | catch (JSException) 21 | { 22 | // Clipboard API unavailable or denied; ignore to avoid breaking UI. 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Menu.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Components 5 | { 6 | public partial class Menu 7 | { 8 | private bool _isVisible = false; 9 | 10 | private Preferences? _preferences; 11 | 12 | protected Preferences? Preferences => _preferences; 13 | 14 | [Parameter] 15 | public bool IsDarkMode { get; set; } 16 | 17 | [Parameter] 18 | public EventCallback DarkModeChanged { get; set; } 19 | 20 | public void ShowMenu(Preferences? preferences = null) 21 | { 22 | _isVisible = true; 23 | _preferences = preferences; 24 | 25 | StateHasChanged(); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /.dcignore: -------------------------------------------------------------------------------- 1 | # Write glob rules for ignored files. 2 | # Check syntax on https://deepcode.freshdesk.com/support/solutions/articles/60000531055-how-can-i-ignore-files-or-directories- 3 | # Check examples on https://github.com/github/gitignore 4 | 5 | # Hidden directories and files 6 | .* 7 | 8 | # Common binary directories and files 9 | [Bb]in/ 10 | [Oo]bj/ 11 | *.exe 12 | *.dll 13 | 14 | # Logs and temporary files 15 | [Tt]emp/ 16 | *.log 17 | 18 | # Build directories 19 | /build/ 20 | /dist/ 21 | /out/ 22 | 23 | # Node modules and package directories 24 | node_modules/ 25 | **/[Pp]ackages/* 26 | 27 | # Various cache directories 28 | *.cache 29 | /saved/ 30 | /intermediates/ 31 | /generated/ 32 | /coverage/ 33 | /tmp/ 34 | 35 | # Specific directory exclusions 36 | /DocProject/Help/html/ 37 | 38 | # Ignore files from .vs directory 39 | .vs/ 40 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/LogForm.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class LogForm 4 | { 5 | public bool Normal => SelectedTypes.Contains("Normal"); 6 | public bool Info => SelectedTypes.Contains("Info"); 7 | public bool Warning => SelectedTypes.Contains("Warning"); 8 | public bool Critical => SelectedTypes.Contains("Critical"); 9 | 10 | public int? LastKnownId { get; set; } 11 | 12 | #pragma warning disable IDE0028 // Simplify collection initialization - the SelectedValues of MudSelect has issues with the type being HashSet but it needs to be. 13 | public IEnumerable SelectedTypes { get; set; } = new HashSet(); 14 | #pragma warning restore IDE0028 // Simplify collection initialization 15 | 16 | public string? Criteria { get; set; } 17 | } 18 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/OtherLayout.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Layout 5 | { 6 | public partial class OtherLayout 7 | { 8 | [CascadingParameter(Name = "DrawerOpen")] 9 | public bool DrawerOpen { get; set; } 10 | 11 | [CascadingParameter(Name = "DrawerOpenChanged")] 12 | public EventCallback DrawerOpenChanged { get; set; } 13 | 14 | [CascadingParameter] 15 | public Preferences? Preferences { get; set; } 16 | 17 | protected async Task OnDrawerOpenChanged(bool value) 18 | { 19 | DrawerOpen = value; 20 | if (DrawerOpenChanged.HasDelegate) 21 | { 22 | await DrawerOpenChanged.InvokeAsync(value); 23 | } 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/Log.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record Log 6 | { 7 | [JsonConstructor] 8 | public Log( 9 | int id, 10 | string message, 11 | long timestamp, 12 | LogType type) 13 | { 14 | Id = id; 15 | Message = message; 16 | Timestamp = timestamp; 17 | Type = type; 18 | } 19 | 20 | [JsonPropertyName("id")] 21 | public int Id { get; } 22 | 23 | [JsonPropertyName("message")] 24 | public string Message { get; } 25 | 26 | [JsonPropertyName("timestamp")] 27 | public long Timestamp { get; } 28 | 29 | [JsonPropertyName("type")] 30 | public LogType Type { get; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/TdExtended.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Web; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.UI 6 | { 7 | public partial class TdExtended : MudTd 8 | { 9 | [Parameter] 10 | public EventCallback OnLongPress { get; set; } 11 | 12 | [Parameter] 13 | public EventCallback OnContextMenu { get; set; } 14 | 15 | protected Task OnLongPressInternal(LongPressEventArgs e) 16 | { 17 | return OnLongPress.InvokeAsync(new CellLongPressEventArgs(e, this)); 18 | } 19 | 20 | protected Task OnContextMenuInternal(MouseEventArgs e) 21 | { 22 | return OnContextMenu.InvokeAsync(new CellMouseEventArgs(e, this)); 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Infrastructure/TestClipboardService.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Services; 2 | using System.Collections.Concurrent; 3 | 4 | namespace Lantean.QBTMud.Test.Infrastructure 5 | { 6 | internal sealed class TestClipboardService : IClipboardService 7 | { 8 | private readonly ConcurrentQueue _writes = new(); 9 | 10 | public Task WriteToClipboard(string text) 11 | { 12 | _writes.Enqueue(text); 13 | return Task.CompletedTask; 14 | } 15 | 16 | public IReadOnlyCollection Entries => _writes.ToArray(); 17 | 18 | public string? PeekLast() 19 | { 20 | return _writes.LastOrDefault(); 21 | } 22 | 23 | public void Clear() 24 | { 25 | while (_writes.TryDequeue(out _)) 26 | { 27 | } 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Filter/PropertyFilterDefinition.cs: -------------------------------------------------------------------------------- 1 | using System.Linq.Expressions; 2 | 3 | namespace Lantean.QBTMud.Filter 4 | { 5 | public record PropertyFilterDefinition 6 | { 7 | public PropertyFilterDefinition(string column, string @operator, object? value) 8 | { 9 | var (expression, propertyType) = ExpressionModifier.CreatePropertySelector(column); 10 | 11 | Column = column; 12 | ColumnType = propertyType; 13 | Operator = @operator; 14 | Value = value; 15 | Expression = expression; 16 | } 17 | 18 | public string Column { get; } 19 | 20 | public Type ColumnType { get; } 21 | 22 | public string Operator { get; set; } 23 | 24 | public object? Value { get; set; } 25 | 26 | public Expression> Expression { get; } 27 | } 28 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/FileRow.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class FileRow 4 | { 5 | public required string OriginalName { get; set; } 6 | public string? NewName { get; set; } 7 | public bool IsFolder { get; set; } 8 | public required string Name { get; set; } 9 | public int Level { get; set; } 10 | public bool Renamed { get; set; } 11 | public string? ErrorMessage { get; set; } 12 | public required string Path { get; set; } 13 | 14 | public override bool Equals(object? obj) 15 | { 16 | if (obj is null) 17 | { 18 | return false; 19 | } 20 | 21 | return ((FileRow)obj).Name == Name; 22 | } 23 | 24 | public override int GetHashCode() 25 | { 26 | return Name.GetHashCode(); 27 | } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SearchPreferences.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class SearchPreferences 4 | { 5 | public string SelectedCategory { get; set; } = SearchForm.AllCategoryId; 6 | 7 | public HashSet SelectedPlugins { get; set; } = new(StringComparer.OrdinalIgnoreCase); 8 | 9 | public SearchInScope SearchIn { get; set; } = SearchInScope.Everywhere; 10 | 11 | public string? FilterText { get; set; } 12 | 13 | public int? MinimumSeeds { get; set; } 14 | 15 | public int? MaximumSeeds { get; set; } 16 | 17 | public double? MinimumSize { get; set; } 18 | 19 | public SearchSizeUnit MinimumSizeUnit { get; set; } = SearchSizeUnit.Mebibytes; 20 | 21 | public double? MaximumSize { get; set; } 22 | 23 | public SearchSizeUnit MaximumSizeUnit { get; set; } = SearchSizeUnit.Gibibytes; 24 | } 25 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/TorrentInfo.razor: -------------------------------------------------------------------------------- 1 | @if (Torrent is null) 2 | { 3 | return; 4 | } 5 | 6 | 7 | @{ 8 | var (icon, color) = DisplayHelpers.GetStateIcon(Torrent.State); 9 | } 10 | 11 | @Torrent.Name 12 | 13 | @DisplayHelpers.Size(Torrent.Size) 14 | 15 | @{ 16 | var value = Torrent.Progress; 17 | var progressColor = value < 1 ? Color.Success : Color.Info; 18 | 19 | @DisplayHelpers.Percentage(value) 20 | 21 | ; 22 | } 23 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/RssTreeNode.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.ObjectModel; 2 | 3 | namespace Lantean.QBTMud.Models 4 | { 5 | public sealed class RssTreeNode 6 | { 7 | public RssTreeNode(string name, string path, bool isFolder, bool isUnread) 8 | { 9 | Name = name; 10 | Path = path; 11 | IsFolder = isFolder; 12 | IsUnread = isUnread; 13 | Children = new Collection(); 14 | } 15 | 16 | public string Name { get; } 17 | 18 | public string Path { get; } 19 | 20 | public bool IsFolder { get; } 21 | 22 | public bool IsUnread { get; } 23 | 24 | public RssFeed? Feed { get; set; } 25 | 26 | public Collection Children { get; } 27 | 28 | public int ArticleCount { get; set; } 29 | 30 | public int UnreadCount { get; set; } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/Category.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Converters; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Lantean.QBitTorrentClient.Models 5 | { 6 | public record Category 7 | { 8 | [JsonConstructor] 9 | public Category( 10 | string name, 11 | string? savePath, 12 | DownloadPathOption? downloadPath) 13 | { 14 | Name = name; 15 | SavePath = savePath; 16 | DownloadPath = downloadPath; 17 | } 18 | 19 | [JsonPropertyName("name")] 20 | public string Name { get; } 21 | 22 | [JsonPropertyName("savePath")] 23 | public string? SavePath { get; } 24 | 25 | [JsonPropertyName("download_path")] 26 | [JsonConverter(typeof(DownloadPathOptionJsonConverter))] 27 | public DownloadPathOption? DownloadPath { get; } 28 | } 29 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/DeleteDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class DeleteDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public int Count { get; set; } 14 | 15 | protected bool DeleteFiles { get; set; } 16 | 17 | protected void Cancel() 18 | { 19 | MudDialog.Cancel(); 20 | } 21 | 22 | protected void Submit() 23 | { 24 | MudDialog.Close(DialogResult.Ok(DeleteFiles)); 25 | } 26 | 27 | protected override Task Submit(KeyboardEvent keyboardEvent) 28 | { 29 | Submit(); 30 | 31 | return Task.CompletedTask; 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ManageTagsDialog.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Add 7 | Remove All 8 | 9 | @foreach (var tag in Tags) 10 | { 11 | var tagRef = tag; 12 | @tag 13 | } 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/SearchForm.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class SearchForm 4 | { 5 | public const string AllCategoryId = "all"; 6 | 7 | public string? SearchText { get; set; } 8 | 9 | public HashSet SelectedPlugins { get; set; } = new(StringComparer.OrdinalIgnoreCase); 10 | 11 | public string SelectedCategory { get; set; } = AllCategoryId; 12 | 13 | public string? FilterText { get; set; } 14 | 15 | public SearchInScope SearchIn { get; set; } = SearchInScope.Everywhere; 16 | 17 | public int? MinimumSeeds { get; set; } 18 | 19 | public int? MaximumSeeds { get; set; } 20 | 21 | public double? MinimumSize { get; set; } 22 | 23 | public SearchSizeUnit MinimumSizeUnit { get; set; } = SearchSizeUnit.Mebibytes; 24 | 25 | public double? MaximumSize { get; set; } 26 | 27 | public SearchSizeUnit MaximumSizeUnit { get; set; } = SearchSizeUnit.Gibibytes; 28 | } 29 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ManageCategoriesDialog.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Add 7 | Remove 8 | 9 | @foreach (var category in Categories) 10 | { 11 | var categoryRef = category; 12 | @categoryRef 13 | } 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/ApplicationCookie.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record ApplicationCookie 6 | { 7 | [JsonConstructor] 8 | public ApplicationCookie(string name, string? domain, string? path, string? value, long? expirationDate) 9 | { 10 | Name = name; 11 | Domain = domain; 12 | Path = path; 13 | Value = value; 14 | ExpirationDate = expirationDate; 15 | } 16 | 17 | [JsonPropertyName("name")] 18 | public string Name { get; } 19 | 20 | [JsonPropertyName("domain")] 21 | public string? Domain { get; } 22 | 23 | [JsonPropertyName("path")] 24 | public string? Path { get; } 25 | 26 | [JsonPropertyName("value")] 27 | public string? Value { get; } 28 | 29 | [JsonPropertyName("expirationDate")] 30 | public long? ExpirationDate { get; } 31 | } 32 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/PeerLog.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record PeerLog 6 | { 7 | [JsonConstructor] 8 | public PeerLog( 9 | int id, 10 | string iPAddress, 11 | long timestamp, 12 | bool blocked, 13 | string reason) 14 | { 15 | Id = id; 16 | IPAddress = iPAddress; 17 | Timestamp = timestamp; 18 | Blocked = blocked; 19 | Reason = reason; 20 | } 21 | 22 | [JsonPropertyName("id")] 23 | public int Id { get; } 24 | 25 | [JsonPropertyName("ip")] 26 | public string IPAddress { get; } 27 | 28 | [JsonPropertyName("timestamp")] 29 | public long Timestamp { get; } 30 | 31 | [JsonPropertyName("blocked")] 32 | public bool Blocked { get; } 33 | 34 | [JsonPropertyName("reason")] 35 | public string Reason { get; } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ConfirmDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class ConfirmDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public string Content { get; set; } = default!; 14 | 15 | [Parameter] 16 | public string? SuccessText { get; set; } = "Ok"; 17 | 18 | [Parameter] 19 | public string? CancelText { get; set; } = "Cancel"; 20 | 21 | protected void Cancel() 22 | { 23 | MudDialog.Cancel(); 24 | } 25 | 26 | protected void Submit() 27 | { 28 | MudDialog.Close(DialogResult.Ok(true)); 29 | } 30 | 31 | protected override Task Submit(KeyboardEvent keyboardEvent) 32 | { 33 | Submit(); 34 | 35 | return Task.CompletedTask; 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/CustomIcons.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud 2 | { 3 | public static class CustomIcons 4 | { 5 | public const string Magnet = @""; 6 | 7 | public const string Random = @""; 8 | 9 | public const string RadioIndeterminate = @""; 10 | } 11 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/RssFeed.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class RssFeed 4 | { 5 | public RssFeed( 6 | bool hasError, 7 | bool isLoading, 8 | string? lastBuildDate, 9 | string? title, 10 | string uid, 11 | string url, 12 | string path) 13 | { 14 | HasError = hasError; 15 | IsLoading = isLoading; 16 | LastBuildDate = lastBuildDate; 17 | Title = title; 18 | Uid = uid; 19 | Url = url; 20 | Path = path; 21 | } 22 | 23 | public bool HasError { get; } 24 | 25 | public bool IsLoading { get; internal set; } 26 | 27 | public string? LastBuildDate { get; } 28 | 29 | public string? Title { get; } 30 | 31 | public string Uid { get; } 32 | 33 | public string Url { get; } 34 | 35 | public string Path { get; } 36 | 37 | public int ArticleCount { get; internal set; } 38 | 39 | public int UnreadCount { get; internal set; } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTagDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @foreach (var tag in Tags) 12 | { 13 | var tagRef = tag; 14 | 15 | 16 | 17 | 18 | } 19 | 20 |
@tag
21 |
22 | 23 | Cancel 24 | Save 25 | 26 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/StringFieldDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class StringFieldDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public string? Label { get; set; } 14 | 15 | [Parameter] 16 | public string? Value { get; set; } 17 | 18 | [Parameter] 19 | public bool Disabled { get; set; } 20 | 21 | protected void ValueChanged(string value) 22 | { 23 | Value = value; 24 | } 25 | 26 | protected void Cancel() 27 | { 28 | MudDialog.Cancel(); 29 | } 30 | 31 | protected void Submit() 32 | { 33 | MudDialog.Close(DialogResult.Ok(Value)); 34 | } 35 | 36 | protected override Task Submit(KeyboardEvent keyboardEvent) 37 | { 38 | Submit(); 39 | 40 | return Task.CompletedTask; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/MultipleFieldDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @foreach (var value in NewValues) 12 | { 13 | var valueRef = value; 14 | 15 | 16 | 17 | 18 | } 19 | 20 |
@value
21 |
22 | 23 | Cancel 24 | Save 25 | 26 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTrackerDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | @foreach (var tracker in Trackers) 12 | { 13 | var trackerRef = tracker; 14 | 15 | 16 | 17 | 18 | } 19 | 20 |
@tracker
21 |
22 | 23 | Cancel 24 | Save 25 | 26 |
-------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/TorrentPeers.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record TorrentPeers 6 | { 7 | [JsonConstructor] 8 | public TorrentPeers( 9 | bool fullUpdate, 10 | IReadOnlyDictionary? peers, 11 | IReadOnlyList? peersRemoved, 12 | int requestId, 13 | bool? showFlags) 14 | { 15 | FullUpdate = fullUpdate; 16 | Peers = peers; 17 | PeersRemoved = peersRemoved; 18 | RequestId = requestId; 19 | ShowFlags = showFlags; 20 | } 21 | 22 | [JsonPropertyName("full_update")] 23 | public bool FullUpdate { get; } 24 | 25 | [JsonPropertyName("peers")] 26 | public IReadOnlyDictionary? Peers { get; } 27 | 28 | [JsonPropertyName("peers_removed")] 29 | public IReadOnlyList? PeersRemoved { get; } 30 | 31 | [JsonPropertyName("rid")] 32 | public int RequestId { get; } 33 | 34 | [JsonPropertyName("show_flags")] 35 | public bool? ShowFlags { get; } 36 | } 37 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Statistics.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient; 2 | using Lantean.QBTMud.Models; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Pages 7 | { 8 | public partial class Statistics 9 | { 10 | [Inject] 11 | protected IApiClient ApiClient { get; set; } = default!; 12 | 13 | [Inject] 14 | protected IDialogService DialogService { get; set; } = default!; 15 | 16 | [Inject] 17 | protected NavigationManager NavigationManager { get; set; } = default!; 18 | 19 | [CascadingParameter] 20 | public MainData? MainData { get; set; } 21 | 22 | [CascadingParameter(Name = "DrawerOpen")] 23 | public bool DrawerOpen { get; set; } 24 | 25 | [CascadingParameter(Name = "RefreshInterval")] 26 | public int RefreshInterval { get; set; } 27 | 28 | [Parameter] 29 | public string? Hash { get; set; } 30 | 31 | protected int ActiveTab { get; set; } = 0; 32 | 33 | protected ServerState? ServerState => MainData?.ServerState; 34 | 35 | protected void NavigateBack() 36 | { 37 | NavigationManager.NavigateToHome(); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ExceptionDialog.razor: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | @if (Exception is null) 5 | { 6 | 7 | 8 | Missing error information. 9 | 10 | 11 | } 12 | else 13 | { 14 | 15 | @Exception.Message 16 | 17 | 18 | @Exception.Source 19 | 20 | 21 | 22 |
23 |                             @Exception.StackTrace
24 |                         
25 |
26 |
27 | } 28 |
29 | 30 |
31 | 32 | Close 33 | 34 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/SubMenuDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class SubMenuDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public UIAction? ParentAction { get; set; } 14 | 15 | [Parameter] 16 | public Dictionary Torrents { get; set; } = default!; 17 | 18 | [Parameter] 19 | public QBitTorrentClient.Models.Preferences? Preferences { get; set; } 20 | 21 | [Parameter] 22 | public IEnumerable Hashes { get; set; } = []; 23 | 24 | [Parameter] 25 | public HashSet Tags { get; set; } = default!; 26 | 27 | [Parameter] 28 | public Dictionary Categories { get; set; } = default!; 29 | 30 | protected Task CloseDialog() 31 | { 32 | MudDialog.Close(); 33 | 34 | return Task.CompletedTask; 35 | } 36 | 37 | protected void Cancel() 38 | { 39 | MudDialog.Cancel(); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Components/UI/TickSwitchTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBTMud.Components.UI; 3 | using Lantean.QBTMud.Test.Infrastructure; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Test.Components.UI 7 | { 8 | public sealed class TickSwitchTests : RazorComponentTestBase 9 | { 10 | [Fact] 11 | public void GIVEN_ValueTrue_WHEN_Rendered_THEN_ShouldUseSuccessIcon() 12 | { 13 | var target = TestContext.Render>(parameters => 14 | { 15 | parameters.Add(p => p.Value, true); 16 | }); 17 | 18 | target.Instance.ThumbIcon.Should().Be(Icons.Material.Filled.Done); 19 | target.Instance.ThumbIconColor.Should().Be(Color.Success); 20 | } 21 | 22 | [Fact] 23 | public void GIVEN_ValueFalse_WHEN_Rendered_THEN_ShouldUseErrorIcon() 24 | { 25 | var target = TestContext.Render>(parameters => 26 | { 27 | parameters.Add(p => p.Value, false); 28 | }); 29 | 30 | target.Instance.ThumbIcon.Should().Be(Icons.Material.Filled.Close); 31 | target.Instance.ThumbIconColor.Should().Be(Color.Error); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/ErrorDisplay.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Components.Dialogs; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components 6 | { 7 | public partial class ErrorDisplay 8 | { 9 | [Inject] 10 | protected IDialogService DialogService { get; set; } = default!; 11 | 12 | [Parameter] 13 | [EditorRequired] 14 | public EnhancedErrorBoundary ErrorBoundary { get; set; } = default!; 15 | 16 | protected IEnumerable Errors => ErrorBoundary.Errors; 17 | 18 | protected async Task ShowException(Exception exception) 19 | { 20 | var parameters = new DialogParameters 21 | { 22 | { nameof(ExceptionDialog.Exception), exception } 23 | }; 24 | 25 | await DialogService.ShowAsync("Error Details", parameters, global::Lantean.QBTMud.Services.DialogWorkflow.FormDialogOptions); 26 | } 27 | 28 | protected async Task ClearErrors() 29 | { 30 | await ErrorBoundary.ClearErrors(); 31 | } 32 | 33 | protected async Task ClearErrorsAndResumeAsync() 34 | { 35 | await ErrorBoundary.RecoverAndClearErrors(); 36 | } 37 | } 38 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/TableDataLongPressEventArgs.cs: -------------------------------------------------------------------------------- 1 | using MudBlazor; 2 | 3 | namespace Lantean.QBTMud.Components.UI 4 | { 5 | public class TableDataLongPressEventArgs : EventArgs 6 | { 7 | // 8 | // Summary: 9 | // The coordinates of the click. 10 | public LongPressEventArgs LongPressEventArgs { get; } 11 | 12 | // 13 | // Summary: 14 | // The row which was clicked. 15 | public MudTd Data { get; } 16 | 17 | // 18 | // Summary: 19 | // The data related to the row which was clicked. 20 | public T? Item { get; } 21 | 22 | // 23 | // Summary: 24 | // Creates a new instance. 25 | // 26 | // Parameters: 27 | // mouseEventArgs: 28 | // The coordinates of the click. 29 | // 30 | // row: 31 | // The row which was context-clicked. 32 | // 33 | // item: 34 | // The data related to the row which was context-clicked. 35 | public TableDataLongPressEventArgs(LongPressEventArgs longPressEventArgs, MudTd data, T? item) 36 | { 37 | LongPressEventArgs = longPressEventArgs; 38 | Data = data; 39 | Item = item; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/BuildInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record BuildInfo 6 | { 7 | [JsonConstructor] 8 | public BuildInfo( 9 | string qTVersion, 10 | string libTorrentVersion, 11 | string boostVersion, 12 | string openSSLVersion, 13 | string zlibVersion, 14 | int bitness) 15 | { 16 | QTVersion = qTVersion; 17 | LibTorrentVersion = libTorrentVersion; 18 | BoostVersion = boostVersion; 19 | OpenSSLVersion = openSSLVersion; 20 | ZLibVersion = zlibVersion; 21 | Bitness = bitness; 22 | } 23 | 24 | [JsonPropertyName("qt")] 25 | public string QTVersion { get; } 26 | 27 | [JsonPropertyName("libtorrent")] 28 | public string LibTorrentVersion { get; } 29 | 30 | [JsonPropertyName("boost")] 31 | public string BoostVersion { get; } 32 | 33 | [JsonPropertyName("openssl")] 34 | public string OpenSSLVersion { get; } 35 | 36 | [JsonPropertyName("zlib")] 37 | public string ZLibVersion { get; } 38 | 39 | [JsonPropertyName("bitness")] 40 | public int Bitness { get; } 41 | } 42 | } -------------------------------------------------------------------------------- /test/Lantean.QBitTorrentClient.Test/Lantean.QBitTorrentClient.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | false 8 | 9 | 10 | 11 | 12 | 13 | all 14 | runtime; build; native; contentfiles; analyzers; buildtransitive 15 | 16 | 17 | 18 | 19 | all 20 | runtime; build; native; contentfiles; analyzers; buildtransitive 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddPeerDialog.razor: -------------------------------------------------------------------------------- 1 | @inherits SubmittableDialog 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | @foreach (var peer in Peers) 13 | { 14 | var peerRef = peer; 15 | 16 | 17 | 18 | 19 | } 20 | 21 |
@peer
22 |
23 | 24 | Cancel 25 | Save 26 | 27 |
-------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/FilterState.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public readonly struct FilterState 4 | { 5 | public FilterState( 6 | string category, 7 | Status status, 8 | string tag, 9 | string tracker, 10 | bool useSubcategories, 11 | string? terms, 12 | TorrentFilterField filterField, 13 | bool useRegex, 14 | bool isRegexValid) 15 | { 16 | Category = category; 17 | Status = status; 18 | Tag = tag; 19 | Tracker = tracker; 20 | UseSubcategories = useSubcategories; 21 | Terms = terms; 22 | FilterField = filterField; 23 | UseRegex = useRegex; 24 | IsRegexValid = isRegexValid; 25 | } 26 | 27 | public string Category { get; } = "all"; 28 | public Status Status { get; } = Status.All; 29 | public string Tag { get; } = "all"; 30 | public string Tracker { get; } = "all"; 31 | public bool UseSubcategories { get; } 32 | public string? Terms { get; } 33 | public TorrentFilterField FilterField { get; } = TorrentFilterField.Name; 34 | public bool UseRegex { get; } 35 | public bool IsRegexValid { get; } = true; 36 | } 37 | } -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Services/PeriodicTimerFactoryTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBTMud.Services; 3 | using System.Threading; 4 | using Xunit; 5 | 6 | namespace Lantean.QBTMud.Test.Services 7 | { 8 | public sealed class PeriodicTimerFactoryTests 9 | { 10 | private readonly PeriodicTimerFactory _target; 11 | 12 | public PeriodicTimerFactoryTests() 13 | { 14 | _target = new PeriodicTimerFactory(); 15 | } 16 | 17 | [Fact] 18 | public async Task GIVEN_ShortPeriod_WHEN_WaitForTick_THEN_ReturnsTrue() 19 | { 20 | await using var timer = _target.Create(TimeSpan.FromMilliseconds(10)); 21 | using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(1)); 22 | 23 | var result = await timer.WaitForNextTickAsync(cts.Token); 24 | 25 | result.Should().BeTrue(); 26 | } 27 | 28 | [Fact] 29 | public async Task GIVEN_DisposedTimer_WHEN_WaitForTick_THEN_ReturnsFalse() 30 | { 31 | await using var timer = _target.Create(TimeSpan.FromMilliseconds(10)); 32 | 33 | await timer.DisposeAsync(); 34 | var result = await timer.WaitForNextTickAsync(CancellationToken.None); 35 | 36 | result.Should().BeFalse(); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/TableDataContextMenuEventArgs.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Web; 2 | using MudBlazor; 3 | 4 | namespace Lantean.QBTMud.Components.UI 5 | { 6 | public class TableDataContextMenuEventArgs : EventArgs 7 | { 8 | // 9 | // Summary: 10 | // The coordinates of the click. 11 | public MouseEventArgs MouseEventArgs { get; } 12 | 13 | // 14 | // Summary: 15 | // The row which was clicked. 16 | public MudTd Data { get; } 17 | 18 | // 19 | // Summary: 20 | // The data related to the row which was clicked. 21 | public T? Item { get; } 22 | 23 | // 24 | // Summary: 25 | // Creates a new instance. 26 | // 27 | // Parameters: 28 | // mouseEventArgs: 29 | // The coordinates of the click. 30 | // 31 | // row: 32 | // The row which was context-clicked. 33 | // 34 | // item: 35 | // The data related to the row which was context-clicked. 36 | public TableDataContextMenuEventArgs(MouseEventArgs mouseEventArgs, MudTd data, T? item) 37 | { 38 | MouseEventArgs = mouseEventArgs; 39 | Data = data; 40 | Item = item; 41 | } 42 | } 43 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SearchPlugin.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SearchPlugin 6 | { 7 | [JsonConstructor] 8 | public SearchPlugin( 9 | bool enabled, 10 | string fullName, 11 | string name, 12 | IReadOnlyList supportedCategories, 13 | string url, 14 | string version) 15 | { 16 | Enabled = enabled; 17 | FullName = fullName; 18 | Name = name; 19 | SupportedCategories = supportedCategories; 20 | Url = url; 21 | Version = version; 22 | } 23 | 24 | [JsonPropertyName("enabled")] 25 | public bool Enabled { get; set; } 26 | 27 | [JsonPropertyName("fullName")] 28 | public string FullName { get; set; } 29 | 30 | [JsonPropertyName("name")] 31 | public string Name { get; set; } 32 | 33 | [JsonPropertyName("supportedCategories")] 34 | public IReadOnlyList SupportedCategories { get; set; } 35 | 36 | [JsonPropertyName("url")] 37 | public string Url { get; set; } 38 | 39 | [JsonPropertyName("version")] 40 | public string Version { get; set; } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Converters/StringFloatJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Lantean.QBitTorrentClient.Converters 6 | { 7 | internal class StringFloatJsonConverter : JsonConverter 8 | { 9 | public override float Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType == JsonTokenType.String) 12 | { 13 | if (float.TryParse(reader.GetString(), NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var value)) 14 | { 15 | return value; 16 | } 17 | 18 | return 0; 19 | } 20 | 21 | if (reader.TokenType == JsonTokenType.Number) 22 | { 23 | if (reader.TryGetSingle(out var value)) 24 | { 25 | return value; 26 | } 27 | 28 | return 0; 29 | } 30 | 31 | return 0; 32 | } 33 | 34 | public override void Write(Utf8JsonWriter writer, float value, JsonSerializerOptions options) 35 | { 36 | writer.WriteStringValue(value.ToString(CultureInfo.InvariantCulture)); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/SliderFieldDialog.razor: -------------------------------------------------------------------------------- 1 | @typeparam T 2 | @inherits SubmittableDialog 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | @if (ValueDisplayFunc is not null) 14 | { 15 | @ValueDisplayFunc(context.Value) 16 | } 17 | else 18 | { 19 | @context.Value 20 | } 21 | 22 | 23 | 24 | 25 | 26 | 27 | Cancel 28 | Save 29 | 30 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/TorrentsListNav.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Lantean.QBTMud.Pages; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Components 7 | { 8 | public partial class TorrentsListNav 9 | { 10 | [Inject] 11 | protected NavigationManager NavigationManager { get; set; } = default!; 12 | 13 | [Parameter] 14 | public IEnumerable? Torrents { get; set; } 15 | 16 | [Parameter] 17 | public string? SelectedTorrent { get; set; } 18 | 19 | [Parameter] 20 | public SortDirection SortDirection { get; set; } 21 | 22 | [Parameter] 23 | public string? SortColumn { get; set; } 24 | 25 | protected IEnumerable? OrderedTorrents => GetOrderedTorrents(); 26 | 27 | private IEnumerable? GetOrderedTorrents() 28 | { 29 | if (Torrents is null) 30 | { 31 | return null; 32 | } 33 | 34 | var sortSelector = TorrentList.ColumnsDefinitions.Find(t => t.Id == SortColumn)?.SortSelector ?? (t => t.Name); 35 | 36 | return Torrents.OrderByDirection(SortDirection, sortSelector); 37 | } 38 | 39 | protected void NavigateBack() 40 | { 41 | NavigationManager.NavigateToHome(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/AddTorrentResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record AddTorrentResult 6 | { 7 | [JsonConstructor] 8 | public AddTorrentResult(int successCount, int failureCount, int pendingCount, IReadOnlyList? addedTorrentIds) 9 | { 10 | SuccessCount = successCount; 11 | FailureCount = failureCount; 12 | PendingCount = pendingCount; 13 | AddedTorrentIds = addedTorrentIds ?? []; 14 | SupportsAsync = true; 15 | } 16 | 17 | public AddTorrentResult(int successCount, int failureCount) 18 | { 19 | SuccessCount = successCount; 20 | FailureCount = failureCount; 21 | AddedTorrentIds = []; 22 | SupportsAsync = false; 23 | } 24 | 25 | [JsonPropertyName("success_count")] 26 | public int SuccessCount { get; } 27 | 28 | [JsonPropertyName("failure_count")] 29 | public int FailureCount { get; } 30 | 31 | [JsonPropertyName("pending_count")] 32 | public int PendingCount { get; } 33 | 34 | [JsonPropertyName("added_torrent_ids")] 35 | public IReadOnlyList AddedTorrentIds { get; } 36 | 37 | [JsonIgnore] 38 | public bool SupportsAsync { get; internal set; } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/RssItem.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record RssItem 6 | { 7 | [JsonConstructor] 8 | public RssItem( 9 | IReadOnlyList? articles, 10 | bool hasError, 11 | bool isLoading, 12 | string? lastBuildDate, 13 | string? title, 14 | string uid, 15 | string url) 16 | { 17 | Articles = articles; 18 | HasError = hasError; 19 | IsLoading = isLoading; 20 | LastBuildDate = lastBuildDate; 21 | Title = title; 22 | Uid = uid; 23 | Url = url; 24 | } 25 | 26 | [JsonPropertyName("articles")] 27 | public IReadOnlyList? Articles { get; } 28 | 29 | [JsonPropertyName("hasError")] 30 | public bool HasError { get; } 31 | 32 | [JsonPropertyName("IsLoading")] 33 | public bool IsLoading { get; } 34 | 35 | [JsonPropertyName("lastBuildDate")] 36 | public string? LastBuildDate { get; } 37 | 38 | [JsonPropertyName("title")] 39 | public string? Title { get; } 40 | 41 | [JsonPropertyName("uid")] 42 | public string Uid { get; } 43 | 44 | [JsonPropertyName("url")] 45 | public string Url { get; } 46 | } 47 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/ListLayout.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Layout 5 | { 6 | public partial class ListLayout 7 | { 8 | [CascadingParameter(Name = "DrawerOpen")] 9 | public bool DrawerOpen { get; set; } 10 | 11 | [CascadingParameter(Name = "DrawerOpenChanged")] 12 | public EventCallback DrawerOpenChanged { get; set; } 13 | 14 | [CascadingParameter(Name = "StatusChanged")] 15 | public EventCallback StatusChanged { get; set; } 16 | 17 | [CascadingParameter(Name = "CategoryChanged")] 18 | public EventCallback CategoryChanged { get; set; } 19 | 20 | [CascadingParameter(Name = "TagChanged")] 21 | public EventCallback TagChanged { get; set; } 22 | 23 | [CascadingParameter(Name = "TrackerChanged")] 24 | public EventCallback TrackerChanged { get; set; } 25 | 26 | [CascadingParameter(Name = "SearchTermChanged")] 27 | public EventCallback SearchTermChanged { get; set; } 28 | 29 | protected async Task OnDrawerOpenChanged(bool value) 30 | { 31 | DrawerOpen = value; 32 | if (DrawerOpenChanged.HasDelegate) 33 | { 34 | await DrawerOpenChanged.InvokeAsync(value); 35 | } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTrackerDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class AddTrackerDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | protected HashSet Trackers { get; } = []; 13 | 14 | protected string? Tracker { get; set; } 15 | 16 | protected void AddTracker() 17 | { 18 | if (string.IsNullOrEmpty(Tracker)) 19 | { 20 | return; 21 | } 22 | Trackers.Add(Tracker); 23 | Tracker = null; 24 | } 25 | 26 | protected void SetTracker(string tracker) 27 | { 28 | Tracker = tracker; 29 | } 30 | 31 | protected void DeleteTracker(string tracker) 32 | { 33 | Trackers.Remove(tracker); 34 | } 35 | 36 | protected void Cancel() 37 | { 38 | MudDialog.Cancel(); 39 | } 40 | 41 | protected void Submit() 42 | { 43 | MudDialog.Close(Trackers); 44 | } 45 | 46 | protected override Task Submit(KeyboardEvent keyboardEvent) 47 | { 48 | Submit(); 49 | 50 | return Task.CompletedTask; 51 | } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/RssArticle.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public class RssArticle 4 | { 5 | public RssArticle( 6 | string feed, 7 | string? category, 8 | string? comments, 9 | string date, 10 | string? description, 11 | string id, 12 | string? link, 13 | string? thumbnail, 14 | string title, 15 | string torrentURL, 16 | bool isRead) 17 | { 18 | Feed = feed; 19 | Category = category; 20 | Comments = comments; 21 | Date = date; 22 | Description = description; 23 | Id = id; 24 | Link = link; 25 | Thumbnail = thumbnail; 26 | Title = title; 27 | TorrentURL = torrentURL; 28 | IsRead = isRead; 29 | } 30 | 31 | public string Feed { get; } 32 | 33 | public string? Category { get; } 34 | 35 | public string? Comments { get; } 36 | 37 | public string Date { get; } 38 | 39 | public string? Description { get; } 40 | 41 | public string Id { get; } 42 | 43 | public string? Link { get; } 44 | 45 | public string? Thumbnail { get; } 46 | 47 | public string Title { get; } 48 | 49 | public string TorrentURL { get; } 50 | 51 | public bool IsRead { get; set; } 52 | } 53 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/UI/FieldSwitch.razor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | 3 | namespace Lantean.QBTMud.Components.UI 4 | { 5 | public partial class FieldSwitch 6 | { 7 | /// 8 | [Parameter] 9 | public bool Value { get; set; } 10 | 11 | /// 12 | [Parameter] 13 | public EventCallback ValueChanged { get; set; } 14 | 15 | /// 16 | [Parameter] 17 | public string? Label { get; set; } 18 | 19 | /// 20 | [Parameter] 21 | public bool Disabled { get; set; } 22 | 23 | /// 24 | [Parameter] 25 | public object? Validation { get; set; } 26 | 27 | /// 28 | [Parameter] 29 | public string? HelperText { get; set; } 30 | 31 | [Parameter(CaptureUnmatchedValues = true)] 32 | public IReadOnlyDictionary? AdditionalAttributes { get; set; } 33 | 34 | protected async Task ValueChangedCallback(bool value) 35 | { 36 | Value = value; 37 | await ValueChanged.InvokeAsync(value); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Converters/CommaSeparatedJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json; 2 | using System.Text.Json.Serialization; 3 | 4 | namespace Lantean.QBitTorrentClient.Converters 5 | { 6 | internal class CommaSeparatedJsonConverter : JsonConverter> 7 | { 8 | public override IReadOnlyList? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 9 | { 10 | if (reader.TokenType == JsonTokenType.Null) 11 | { 12 | return null; 13 | } 14 | 15 | if (reader.TokenType != JsonTokenType.String) 16 | { 17 | throw new JsonException("Must be of type string."); 18 | } 19 | 20 | List list; 21 | var value = reader.GetString(); 22 | if (string.IsNullOrEmpty(value)) 23 | { 24 | list = []; 25 | } 26 | else 27 | { 28 | list = [.. value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)]; 29 | } 30 | 31 | return list.AsReadOnly(); 32 | } 33 | 34 | public override void Write(Utf8JsonWriter writer, IReadOnlyList value, JsonSerializerOptions options) 35 | { 36 | var output = string.Join(',', value); 37 | 38 | writer.WriteStringValue(output); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Converters/SaveLocationJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Lantean.QBitTorrentClient.Converters 6 | { 7 | internal class SaveLocationJsonConverter : JsonConverter 8 | { 9 | public override SaveLocation Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | if (reader.TokenType == JsonTokenType.String) 12 | { 13 | return SaveLocation.Create(reader.GetString()); 14 | } 15 | 16 | if (reader.TokenType == JsonTokenType.Number) 17 | { 18 | return SaveLocation.Create(reader.GetInt32()); 19 | } 20 | 21 | throw new JsonException($"Unsupported token type {reader.TokenType}"); 22 | } 23 | 24 | public override void Write(Utf8JsonWriter writer, SaveLocation value, JsonSerializerOptions options) 25 | { 26 | if (value.IsWatchedFolder) 27 | { 28 | writer.WriteNumberValue(0); 29 | } 30 | else if (value.IsDefaultFolder) 31 | { 32 | writer.WriteNumberValue(1); 33 | } 34 | else if (value.SavePath is not null) 35 | { 36 | writer.WriteStringValue(value.SavePath); 37 | } 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/Lantean.QBTMud.lib.module.js: -------------------------------------------------------------------------------- 1 | export function beforeWebStart(options, extensions) { 2 | beforeStart(options, extensions); 3 | } 4 | 5 | export function afterWebStarted(blazor) { 6 | afterStarted(blazor) 7 | } 8 | 9 | export function beforeStart(options, extensions) { 10 | console.log("Injecting longpress.js"); 11 | 12 | const element = document.createElement('script'); 13 | element.src = "js/longpress.js"; 14 | element.async = true; 15 | document.body.appendChild(element); 16 | } 17 | 18 | export function afterStarted(blazor) { 19 | console.log("Registering longpress.js"); 20 | 21 | blazor.registerCustomEventType('longpress', { 22 | createEventArgs: event => { 23 | return { 24 | bubbles: event.bubbles, 25 | cancelable: event.cancelable, 26 | screenX: event.detail.screenX, 27 | screenY: event.detail.screenY, 28 | clientX: event.detail.clientX, 29 | clientY: event.detail.clientY, 30 | offsetX: event.detail.offsetX, 31 | offsetY: event.detail.offsetY, 32 | pageX: event.detail.pageX, 33 | pageY: event.detail.pageY, 34 | sourceElement: event.srcElement.localName, 35 | targetElement: event.target.localName, 36 | timeStamp: event.timeStamp, 37 | type: event.type, 38 | }; 39 | } 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/GlobalTransferInfo.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record GlobalTransferInfo 4 | { 5 | public GlobalTransferInfo( 6 | string connectionStatus, 7 | int dHTNodes, 8 | long downloadInfoData, 9 | long downloadInfoSpeed, 10 | long downloadRateLimit, 11 | long uploadInfoData, 12 | long uploadInfoSpeed, 13 | long uploadRateLimit) 14 | { 15 | ConnectionStatus = connectionStatus; 16 | DHTNodes = dHTNodes; 17 | DownloadInfoData = downloadInfoData; 18 | DownloadInfoSpeed = downloadInfoSpeed; 19 | DownloadRateLimit = downloadRateLimit; 20 | UploadInfoData = uploadInfoData; 21 | UploadInfoSpeed = uploadInfoSpeed; 22 | UploadRateLimit = uploadRateLimit; 23 | } 24 | 25 | public GlobalTransferInfo() 26 | { 27 | ConnectionStatus = "Unknown"; 28 | } 29 | 30 | public string ConnectionStatus { get; set; } 31 | 32 | public int DHTNodes { get; set; } 33 | 34 | public long DownloadInfoData { get; set; } 35 | 36 | public long DownloadInfoSpeed { get; set; } 37 | 38 | public long DownloadRateLimit { get; set; } 39 | 40 | public long UploadInfoData { get; set; } 41 | 42 | public long UploadInfoSpeed { get; set; } 43 | 44 | public long UploadRateLimit { get; set; } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Helpers/EventArgsExtensions.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components.Web; 2 | 3 | namespace Lantean.QBTMud.Helpers 4 | { 5 | public static class EventArgsExtensions 6 | { 7 | public static EventArgs NormalizeForContextMenu(this EventArgs eventArgs) 8 | { 9 | ArgumentNullException.ThrowIfNull(eventArgs); 10 | 11 | if (eventArgs is LongPressEventArgs longPressEventArgs) 12 | { 13 | return longPressEventArgs.ToMouseEventArgs(); 14 | } 15 | 16 | return eventArgs; 17 | } 18 | 19 | public static MouseEventArgs ToMouseEventArgs(this LongPressEventArgs longPressEventArgs) 20 | { 21 | ArgumentNullException.ThrowIfNull(longPressEventArgs); 22 | 23 | return new MouseEventArgs 24 | { 25 | Button = 2, 26 | Buttons = 2, 27 | ClientX = longPressEventArgs.ClientX, 28 | ClientY = longPressEventArgs.ClientY, 29 | OffsetX = longPressEventArgs.OffsetX, 30 | OffsetY = longPressEventArgs.OffsetY, 31 | PageX = longPressEventArgs.PageX, 32 | PageY = longPressEventArgs.PageY, 33 | ScreenX = longPressEventArgs.ScreenX, 34 | ScreenY = longPressEventArgs.ScreenY, 35 | Type = longPressEventArgs.Type ?? "contextmenu", 36 | Detail = -1, 37 | }; 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Lantean.QBTMud.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | net10.0 5 | enable 6 | enable 7 | true 8 | false 9 | true 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | all 19 | runtime; build; native; contentfiles; analyzers; buildtransitive 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTorrentFileDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using Microsoft.AspNetCore.Components.Forms; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Components.Dialogs 7 | { 8 | public partial class AddTorrentFileDialog 9 | { 10 | [CascadingParameter] 11 | private IMudDialogInstance MudDialog { get; set; } = default!; 12 | 13 | private List Files { get; set; } = []; 14 | 15 | protected AddTorrentOptions TorrentOptions { get; set; } = default!; 16 | 17 | protected void UploadFiles(IReadOnlyList files) 18 | { 19 | Files = files.ToList(); 20 | } 21 | 22 | protected void Cancel() 23 | { 24 | MudDialog.Cancel(); 25 | } 26 | 27 | protected void Submit() 28 | { 29 | if (Files.Count == 0) 30 | { 31 | MudDialog.Cancel(); 32 | return; 33 | } 34 | 35 | var options = new AddTorrentFileOptions(Files, TorrentOptions.GetTorrentOptions()); 36 | MudDialog.Close(DialogResult.Ok(options)); 37 | } 38 | 39 | protected void Remove(IBrowserFile file) 40 | { 41 | Files.Remove(file); 42 | } 43 | 44 | protected override Task Submit(KeyboardEvent keyboardEvent) 45 | { 46 | Submit(); 47 | 48 | return Task.CompletedTask; 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/images/qbittorrent-tray.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTorrentLinkDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class AddTorrentLinkDialog : SubmittableDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public string? Url { get; set; } 14 | 15 | protected MudTextField? UrlsTextField { get; set; } 16 | 17 | protected string? Urls { get; set; } 18 | 19 | protected AddTorrentOptions TorrentOptions { get; set; } = default!; 20 | 21 | protected override void OnInitialized() 22 | { 23 | if (Url is not null) 24 | { 25 | Urls = Url; 26 | } 27 | } 28 | 29 | protected void Cancel() 30 | { 31 | MudDialog.Cancel(); 32 | } 33 | 34 | protected void Submit() 35 | { 36 | if (Urls is null) 37 | { 38 | MudDialog.Cancel(); 39 | return; 40 | } 41 | var options = new AddTorrentLinkOptions(Urls, TorrentOptions.GetTorrentOptions()); 42 | MudDialog.Close(DialogResult.Ok(options)); 43 | } 44 | 45 | protected override Task Submit(KeyboardEvent keyboardEvent) 46 | { 47 | Submit(); 48 | 49 | return Task.CompletedTask; 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/FileData.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record FileData 6 | { 7 | [JsonConstructor] 8 | public FileData( 9 | int index, 10 | string name, 11 | long size, 12 | float progress, 13 | Priority priority, 14 | bool isSeed, 15 | IReadOnlyList pieceRange, 16 | float availability) 17 | { 18 | Index = index; 19 | Name = name; 20 | Size = size; 21 | Progress = progress; 22 | Priority = priority; 23 | IsSeed = isSeed; 24 | PieceRange = pieceRange ?? []; 25 | Availability = availability; 26 | } 27 | 28 | [JsonPropertyName("index")] 29 | public int Index { get; } 30 | 31 | [JsonPropertyName("name")] 32 | public string Name { get; } 33 | 34 | [JsonPropertyName("size")] 35 | public long Size { get; } 36 | 37 | [JsonPropertyName("progress")] 38 | public float Progress { get; } 39 | 40 | [JsonPropertyName("priority")] 41 | public Priority Priority { get; } 42 | 43 | [JsonPropertyName("is_seed")] 44 | public bool IsSeed { get; } 45 | 46 | [JsonPropertyName("piece_range")] 47 | public IReadOnlyList PieceRange { get; } 48 | 49 | [JsonPropertyName("availability")] 50 | public float Availability { get; } 51 | } 52 | } -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Infrastructure/RazorComponentTestBase.cs: -------------------------------------------------------------------------------- 1 | using Bunit; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Test.Infrastructure 5 | { 6 | public abstract class RazorComponentTestBase : RazorComponentTestBase where T : IComponent 7 | { 8 | protected static IRenderedComponent FindComponentByTestId(IRenderedComponent target, string testId) where TComponent : IComponent 9 | { 10 | return target.FindComponents().First(component => component.FindAll($"[data-test-id='{testId}']").Count > 0); 11 | } 12 | } 13 | 14 | public abstract class RazorComponentTestBase : IAsyncDisposable 15 | { 16 | private bool _disposedValue; 17 | 18 | internal ComponentTestContext TestContext { get; private set; } = new ComponentTestContext(); 19 | 20 | protected virtual ValueTask Dispose(bool disposing) 21 | { 22 | if (!_disposedValue) 23 | { 24 | if (disposing) 25 | { 26 | TestContext.Dispose(); 27 | } 28 | 29 | _disposedValue = true; 30 | } 31 | 32 | return ValueTask.CompletedTask; 33 | } 34 | 35 | public async ValueTask DisposeAsync() 36 | { 37 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 38 | await Dispose(disposing: true); 39 | GC.SuppressFinalize(this); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/PeriodicTimerFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace Lantean.QBTMud.Services 6 | { 7 | /// 8 | /// Creates timers that wrap . 9 | /// 10 | public sealed class PeriodicTimerFactory : IPeriodicTimerFactory 11 | { 12 | public IPeriodicTimer Create(TimeSpan period) 13 | { 14 | return new PeriodicTimerAdapter(period); 15 | } 16 | 17 | private sealed class PeriodicTimerAdapter : IPeriodicTimer 18 | { 19 | private readonly PeriodicTimer _timer; 20 | private bool _disposedValue; 21 | 22 | public PeriodicTimerAdapter(TimeSpan period) 23 | { 24 | _timer = new PeriodicTimer(period); 25 | } 26 | 27 | public async Task WaitForNextTickAsync(CancellationToken cancellationToken) 28 | { 29 | if (_disposedValue) 30 | { 31 | return false; 32 | } 33 | 34 | return await _timer.WaitForNextTickAsync(cancellationToken); 35 | } 36 | 37 | public async ValueTask DisposeAsync() 38 | { 39 | if (_disposedValue) 40 | { 41 | return; 42 | } 43 | 44 | _disposedValue = true; 45 | _timer.Dispose(); 46 | await Task.CompletedTask; 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Infrastructure/ComponentTestContextTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Moq; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Test.Infrastructure 7 | { 8 | public class ComponentTestContextTests 9 | { 10 | [Fact] 11 | public async Task LocalStorageStub_RoundTripsValues() 12 | { 13 | using var context = new ComponentTestContext(); 14 | 15 | await context.LocalStorage.SetItemAsync("Number", 42); 16 | 17 | var value = await context.LocalStorage.GetItemAsync("Number"); 18 | value.Should().Be(42); 19 | } 20 | 21 | [Fact] 22 | public async Task ClipboardStub_RecordsWrites() 23 | { 24 | using var context = new ComponentTestContext(); 25 | 26 | await context.Clipboard.WriteToClipboard("hello"); 27 | await context.Clipboard.WriteToClipboard("world"); 28 | 29 | context.Clipboard.Entries.Should().ContainInOrder("hello", "world"); 30 | context.Clipboard.PeekLast().Should().Be("world"); 31 | } 32 | 33 | [Fact] 34 | public void SnackbarMock_ReplacesRegisteredService() 35 | { 36 | using var context = new ComponentTestContext(); 37 | 38 | var mock = context.UseSnackbarMock(MockBehavior.Loose); 39 | var resolved = context.Services.GetRequiredService(); 40 | 41 | ReferenceEquals(resolved, mock.Object).Should().BeTrue(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/SubmittableDialog.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Lantean.QBTMud.Services; 3 | using Microsoft.AspNetCore.Components; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public abstract class SubmittableDialog : ComponentBase, IAsyncDisposable 8 | { 9 | private bool _disposedValue; 10 | 11 | [Inject] 12 | protected IKeyboardService KeyboardService { get; set; } = default!; 13 | 14 | protected override async Task OnAfterRenderAsync(bool firstRender) 15 | { 16 | if (firstRender) 17 | { 18 | await KeyboardService.RegisterKeypressEvent("Enter", k => Submit(k)); 19 | await KeyboardService.Focus(); 20 | } 21 | } 22 | 23 | protected abstract Task Submit(KeyboardEvent keyboardEvent); 24 | 25 | protected virtual async ValueTask DisposeAsync(bool disposing) 26 | { 27 | if (!_disposedValue) 28 | { 29 | if (disposing) 30 | { 31 | await KeyboardService.UnregisterKeypressEvent("Enter"); 32 | await KeyboardService.UnFocus(); 33 | } 34 | 35 | _disposedValue = true; 36 | } 37 | } 38 | 39 | public async ValueTask DisposeAsync() 40 | { 41 | // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method 42 | await DisposeAsync(disposing: true); 43 | GC.SuppressFinalize(this); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/DetailsLayout.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Layout 6 | { 7 | public partial class DetailsLayout 8 | { 9 | [CascadingParameter(Name = "DrawerOpen")] 10 | public bool DrawerOpen { get; set; } 11 | 12 | [CascadingParameter(Name = "DrawerOpenChanged")] 13 | public EventCallback DrawerOpenChanged { get; set; } 14 | 15 | [CascadingParameter] 16 | public IEnumerable? Torrents { get; set; } 17 | 18 | [CascadingParameter(Name = "SortColumn")] 19 | public string? SortColumn { get; set; } 20 | 21 | [CascadingParameter(Name = "SortDirection")] 22 | public SortDirection SortDirection { get; set; } 23 | 24 | protected string? SelectedTorrent { get; set; } 25 | 26 | protected override void OnParametersSet() 27 | { 28 | if (Body?.Target is not RouteView routeView || routeView.RouteData.RouteValues is null) 29 | { 30 | return; 31 | } 32 | 33 | if (routeView.RouteData.RouteValues.TryGetValue("hash", out var hash)) 34 | { 35 | SelectedTorrent = hash?.ToString(); 36 | } 37 | } 38 | 39 | protected async Task OnDrawerOpenChanged(bool value) 40 | { 41 | DrawerOpen = value; 42 | if (DrawerOpenChanged.HasDelegate) 43 | { 44 | await DrawerOpenChanged.InvokeAsync(value); 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Components/MenuTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Bunit; 3 | using Lantean.QBitTorrentClient.Models; 4 | using Lantean.QBTMud.Components; 5 | using Lantean.QBTMud.Test.Infrastructure; 6 | using Microsoft.AspNetCore.Components.Web; 7 | using MudBlazor; 8 | using System.Text.Json; 9 | 10 | namespace Lantean.QBTMud.Test.Components 11 | { 12 | public sealed class MenuTests : RazorComponentTestBase 13 | { 14 | private readonly IRenderedComponent _target; 15 | 16 | public MenuTests() 17 | { 18 | TestContext.UseApiClientMock(); 19 | TestContext.UseSnackbarMock(); 20 | _target = TestContext.Render(); 21 | } 22 | 23 | [Fact] 24 | public void GIVEN_MenuHidden_WHEN_Rendered_THEN_NoMenuShown() 25 | { 26 | _target.FindComponents().Should().BeEmpty(); 27 | } 28 | 29 | [Fact] 30 | public async Task GIVEN_ShowMenuCalled_WHEN_Rendered_THEN_MenuVisibleWithPreferences() 31 | { 32 | var preferences = JsonSerializer.Deserialize("{\"rss_processing_enabled\":true}")!; 33 | 34 | await _target.InvokeAsync(() => _target.Instance.ShowMenu(preferences)); 35 | 36 | _target.WaitForState(() => _target.FindComponents().Count == 1); 37 | 38 | var menu = _target.FindComponent(); 39 | menu.Should().NotBeNull(); 40 | menu.Instance.Icon.Should().Be(Icons.Material.Filled.MoreVert); 41 | menu.Instance.Disabled.Should().BeFalse(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddPeerDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using Lantean.QBTMud.Models; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Components.Dialogs 7 | { 8 | public partial class AddPeerDialog 9 | { 10 | [CascadingParameter] 11 | public IMudDialogInstance MudDialog { get; set; } = default!; 12 | 13 | protected HashSet Peers { get; } = []; 14 | 15 | protected string? IP { get; set; } 16 | 17 | protected int? Port { get; set; } 18 | 19 | protected void AddTracker() 20 | { 21 | if (string.IsNullOrEmpty(IP) || !Port.HasValue) 22 | { 23 | return; 24 | } 25 | Peers.Add(new PeerId(IP, Port.Value)); 26 | IP = null; 27 | Port = null; 28 | } 29 | 30 | protected void SetIP(string value) 31 | { 32 | IP = value; 33 | } 34 | 35 | protected void SetPort(int? value) 36 | { 37 | Port = value; 38 | } 39 | 40 | protected void DeletePeer(PeerId peer) 41 | { 42 | Peers.Remove(peer); 43 | } 44 | 45 | protected void Cancel() 46 | { 47 | MudDialog.Cancel(); 48 | } 49 | 50 | protected void Submit() 51 | { 52 | MudDialog.Close(Peers); 53 | } 54 | 55 | protected override Task Submit(KeyboardEvent keyboardEvent) 56 | { 57 | Submit(); 58 | 59 | return Task.CompletedTask; 60 | } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Converters/NullableStringFloatJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Lantean.QBitTorrentClient.Converters 6 | { 7 | internal sealed class NullableStringFloatJsonConverter : JsonConverter 8 | { 9 | public override float? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | return reader.TokenType switch 12 | { 13 | JsonTokenType.Null => null, 14 | JsonTokenType.String => ParseString(reader.GetString()), 15 | JsonTokenType.Number => reader.TryGetSingle(out var number) ? number : null, 16 | _ => null 17 | }; 18 | } 19 | 20 | public override void Write(Utf8JsonWriter writer, float? value, JsonSerializerOptions options) 21 | { 22 | if (!value.HasValue) 23 | { 24 | writer.WriteNullValue(); 25 | return; 26 | } 27 | 28 | writer.WriteStringValue(value.Value.ToString(CultureInfo.InvariantCulture)); 29 | } 30 | 31 | private static float? ParseString(string? value) 32 | { 33 | if (string.IsNullOrWhiteSpace(value) || value == "-") 34 | { 35 | return null; 36 | } 37 | 38 | return float.TryParse(value, NumberStyles.Float | NumberStyles.AllowThousands, CultureInfo.InvariantCulture, out var parsed) 39 | ? parsed 40 | : null; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/Lantean.QBitTorrentClient.Test/Converters/NullableStringFloatJsonConverterTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBitTorrentClient.Converters; 3 | using System.Text.Json; 4 | 5 | namespace Lantean.QBitTorrentClient.Test.Converters 6 | { 7 | public class NullableStringFloatJsonConverterTests 8 | { 9 | private static JsonSerializerOptions CreateOptions() 10 | { 11 | var options = new JsonSerializerOptions(); 12 | options.Converters.Add(new NullableStringFloatJsonConverter()); 13 | return options; 14 | } 15 | 16 | [Fact] 17 | public async Task GIVEN_StringDash_WHEN_Read_THEN_ShouldReturnNull() 18 | { 19 | var options = CreateOptions(); 20 | 21 | var value = JsonSerializer.Deserialize("\"-\"", options); 22 | 23 | value.Should().BeNull(); 24 | await Task.CompletedTask; 25 | } 26 | 27 | [Fact] 28 | public async Task GIVEN_StringNumber_WHEN_Read_THEN_ShouldReturnValue() 29 | { 30 | var options = CreateOptions(); 31 | 32 | var value = JsonSerializer.Deserialize("\"123.5\"", options); 33 | 34 | value.Should().Be(123.5f); 35 | await Task.CompletedTask; 36 | } 37 | 38 | [Fact] 39 | public async Task GIVEN_Value_WHEN_Write_THEN_ShouldEmitString() 40 | { 41 | var options = CreateOptions(); 42 | 43 | var json = JsonSerializer.Serialize((float?)1.25f, options); 44 | 45 | json.Should().Be("\"1.25\""); 46 | await Task.CompletedTask; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Login.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient; 2 | using Lantean.QBTMud.Models; 3 | using Microsoft.AspNetCore.Components; 4 | using Microsoft.AspNetCore.Components.Forms; 5 | using System.Net; 6 | 7 | namespace Lantean.QBTMud.Pages 8 | { 9 | public partial class Login 10 | { 11 | [Inject] 12 | protected IApiClient ApiClient { get; set; } = default!; 13 | 14 | [Inject] 15 | protected NavigationManager NavigationManager { get; set; } = default!; 16 | 17 | protected LoginForm Model { get; set; } = new LoginForm(); 18 | 19 | protected string? ApiError { get; set; } 20 | 21 | protected Task LoginClick(EditContext context) 22 | { 23 | return DoLogin(Model.Username, Model.Password); 24 | } 25 | 26 | private async Task DoLogin(string username, string password) 27 | { 28 | try 29 | { 30 | await ApiClient.Login(username, password); 31 | 32 | NavigationManager.NavigateToHome(); 33 | } 34 | catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.BadRequest) 35 | { 36 | ApiError = "Invalid username or password."; 37 | } 38 | catch (HttpRequestException exception) when (exception.StatusCode == HttpStatusCode.Forbidden) 39 | { 40 | ApiError = "Requests from this client are currently unavailable."; 41 | } 42 | catch 43 | { 44 | ApiError = "Unable to communicate with the qBittorrent API."; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Tags.razor: -------------------------------------------------------------------------------- 1 | @page "/tags" 2 | @layout OtherLayout 3 | 4 |
5 |
6 | 7 | @if (!DrawerOpen) 8 | { 9 | 10 | 11 | } 12 | Tags 13 | 14 | 15 | 16 |
17 | 18 |
19 | 27 |
28 |
29 | 30 | @code { 31 | private RenderFragment> ActionsColumn 32 | { 33 | get 34 | { 35 | return context => __builder => 36 | { 37 | var value = (string?)context.GetValue(); 38 | 39 | 40 | 41 | ; 42 | }; 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/CategoryPropertiesDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient; 2 | using Lantean.QBTMud.Models; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Components.Dialogs 7 | { 8 | public partial class CategoryPropertiesDialog 9 | { 10 | private string _savePath = string.Empty; 11 | 12 | [CascadingParameter] 13 | private IMudDialogInstance MudDialog { get; set; } = default!; 14 | 15 | [Inject] 16 | protected IApiClient ApiClient { get; set; } = default!; 17 | 18 | [Parameter] 19 | public string? Category { get; set; } 20 | 21 | [Parameter] 22 | public string? SavePath { get; set; } 23 | 24 | protected override async Task OnInitializedAsync() 25 | { 26 | var preferences = await ApiClient.GetApplicationPreferences(); 27 | _savePath = preferences.SavePath; 28 | 29 | SavePath ??= _savePath; 30 | } 31 | 32 | protected void Cancel() 33 | { 34 | MudDialog.Cancel(); 35 | } 36 | 37 | protected void Submit() 38 | { 39 | if (Category is null) 40 | { 41 | return; 42 | } 43 | 44 | if (string.IsNullOrEmpty(SavePath)) 45 | { 46 | SavePath = _savePath; 47 | } 48 | 49 | MudDialog.Close(DialogResult.Ok(new Category(Category, SavePath))); 50 | } 51 | 52 | protected override Task Submit(KeyboardEvent keyboardEvent) 53 | { 54 | Submit(); 55 | 56 | return Task.CompletedTask; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Converters/DownloadPathOptionJsonConverter.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using System.Text.Json; 3 | using System.Text.Json.Serialization; 4 | 5 | namespace Lantean.QBitTorrentClient.Converters 6 | { 7 | internal sealed class DownloadPathOptionJsonConverter : JsonConverter 8 | { 9 | public override DownloadPathOption? Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 10 | { 11 | return reader.TokenType switch 12 | { 13 | JsonTokenType.Null => null, 14 | JsonTokenType.False => new DownloadPathOption(false, null), 15 | JsonTokenType.True => new DownloadPathOption(true, null), 16 | JsonTokenType.String => new DownloadPathOption(true, reader.GetString()), 17 | _ => throw new JsonException($"Unexpected token {reader.TokenType} when parsing download_path.") 18 | }; 19 | } 20 | 21 | public override void Write(Utf8JsonWriter writer, DownloadPathOption? value, JsonSerializerOptions options) 22 | { 23 | if (value is null) 24 | { 25 | writer.WriteNullValue(); 26 | return; 27 | } 28 | 29 | if (!value.Enabled) 30 | { 31 | writer.WriteBooleanValue(false); 32 | return; 33 | } 34 | 35 | if (string.IsNullOrWhiteSpace(value.Path)) 36 | { 37 | writer.WriteBooleanValue(true); 38 | return; 39 | } 40 | 41 | writer.WriteStringValue(value.Path); 42 | } 43 | } 44 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/AddTagDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient; 2 | using Lantean.QBTMud.Models; 3 | using Microsoft.AspNetCore.Components; 4 | using MudBlazor; 5 | 6 | namespace Lantean.QBTMud.Components.Dialogs 7 | { 8 | public partial class AddTagDialog 9 | { 10 | [Inject] 11 | protected IApiClient ApiClient { get; set; } = default!; 12 | 13 | [Inject] 14 | protected IDialogService DialogService { get; set; } = default!; 15 | 16 | [CascadingParameter] 17 | private IMudDialogInstance MudDialog { get; set; } = default!; 18 | 19 | protected HashSet Tags { get; } = []; 20 | 21 | protected string? Tag { get; set; } 22 | 23 | protected void AddTag() 24 | { 25 | if (string.IsNullOrEmpty(Tag)) 26 | { 27 | return; 28 | } 29 | Tags.Add(Tag); 30 | Tag = null; 31 | } 32 | 33 | protected void SetTag(string tag) 34 | { 35 | Tag = tag; 36 | } 37 | 38 | protected void DeleteTag(string tag) 39 | { 40 | Tags.Remove(tag); 41 | } 42 | 43 | protected void Cancel() 44 | { 45 | MudDialog.Cancel(); 46 | } 47 | 48 | protected void Submit() 49 | { 50 | if (Tags.Count == 0 && Tag is not null) 51 | { 52 | Tags.Add(Tag); 53 | } 54 | MudDialog.Close(Tags); 55 | } 56 | 57 | protected override Task Submit(KeyboardEvent keyboardEvent) 58 | { 59 | Submit(); 60 | 61 | return Task.CompletedTask; 62 | } 63 | } 64 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/TorrentOptionsDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class TorrentOptionsDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | [EditorRequired] 14 | public string Hash { get; set; } = default!; 15 | 16 | [CascadingParameter] 17 | public MainData MainData { get; set; } = default!; 18 | 19 | [CascadingParameter] 20 | public QBitTorrentClient.Models.Preferences Preferences { get; set; } = default!; 21 | 22 | protected bool AutomaticTorrentManagement { get; set; } 23 | 24 | protected string? SavePath { get; set; } 25 | 26 | protected string? TempPath { get; set; } 27 | 28 | protected override void OnInitialized() 29 | { 30 | if (!MainData.Torrents.TryGetValue(Hash, out var torrent)) 31 | { 32 | return; 33 | } 34 | 35 | var tempPath = Preferences.TempPath; 36 | 37 | AutomaticTorrentManagement = torrent.AutomaticTorrentManagement; 38 | SavePath = torrent.SavePath; 39 | TempPath = tempPath; 40 | } 41 | 42 | protected void Cancel() 43 | { 44 | MudDialog.Cancel(); 45 | } 46 | 47 | protected void Submit() 48 | { 49 | MudDialog.Close(); 50 | } 51 | 52 | protected override Task Submit(KeyboardEvent keyboardEvent) 53 | { 54 | Submit(); 55 | 56 | return Task.CompletedTask; 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/MultipartFormDataContentExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Lantean.QBitTorrentClient 4 | { 5 | public static class MultipartFormDataContentExtensions 6 | { 7 | public static void AddString(this MultipartFormDataContent content, string name, string value) 8 | { 9 | content.Add(new StringContent(value), name); 10 | } 11 | 12 | public static void AddString(this MultipartFormDataContent content, string name, bool value) 13 | { 14 | content.AddString(name, value ? "true" : "false"); 15 | } 16 | 17 | public static void AddString(this MultipartFormDataContent content, string name, int value) 18 | { 19 | content.AddString(name, value.ToString(CultureInfo.InvariantCulture)); 20 | } 21 | 22 | public static void AddString(this MultipartFormDataContent content, string name, long value) 23 | { 24 | content.AddString(name, value.ToString(CultureInfo.InvariantCulture)); 25 | } 26 | 27 | public static void AddString(this MultipartFormDataContent content, string name, float value) 28 | { 29 | content.AddString(name, value.ToString(CultureInfo.InvariantCulture)); 30 | } 31 | 32 | public static void AddString(this MultipartFormDataContent content, string name, Enum value) 33 | { 34 | content.AddString(name, value.ToString()); 35 | } 36 | 37 | public static void AddString(this MultipartFormDataContent content, string name, DateTimeOffset value, bool useSeconds = true) 38 | { 39 | content.AddString(name, useSeconds ? value.ToUnixTimeSeconds() : value.ToUnixTimeMilliseconds()); 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/EnhancedErrorBoundary.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using Microsoft.AspNetCore.Components.Rendering; 3 | using System.Collections.ObjectModel; 4 | using System.Runtime.ExceptionServices; 5 | 6 | namespace Lantean.QBTMud.Components 7 | { 8 | public class EnhancedErrorBoundary : ErrorBoundaryBase 9 | { 10 | private readonly ObservableCollection _exceptions = []; 11 | 12 | public bool HasErrored => CurrentException != null; 13 | 14 | [Parameter] 15 | public EventCallback OnClear { get; set; } 16 | 17 | [Parameter] 18 | public bool Disabled { get; set; } 19 | 20 | [Inject] 21 | public ILogger Logger { get; set; } = default!; 22 | 23 | protected override Task OnErrorAsync(Exception exception) 24 | { 25 | Logger.LogError(exception, "An application error occurred: {Message}.", exception.Message); 26 | _exceptions.Add(exception); 27 | 28 | if (Disabled) 29 | { 30 | ExceptionDispatchInfo.Capture(exception).Throw(); 31 | } 32 | 33 | return Task.CompletedTask; 34 | } 35 | 36 | public Task RecoverAndClearErrors() 37 | { 38 | Recover(); 39 | 40 | return ClearErrors(); 41 | } 42 | 43 | public async Task ClearErrors() 44 | { 45 | _exceptions.Clear(); 46 | await OnClear.InvokeAsync(); 47 | } 48 | 49 | public IReadOnlyList Errors => _exceptions.AsReadOnly(); 50 | 51 | protected override void BuildRenderTree(RenderTreeBuilder builder) 52 | { 53 | builder.AddContent(0, ChildContent); 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/About.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Pages 5 | { 6 | public partial class About 7 | { 8 | [Inject] 9 | protected NavigationManager NavigationManager { get; set; } = default!; 10 | 11 | [Inject] 12 | protected IApiClient ApiClient { get; set; } = default!; 13 | 14 | [CascadingParameter(Name = "DrawerOpen")] 15 | public bool DrawerOpen { get; set; } 16 | 17 | [CascadingParameter(Name = "Version")] 18 | public string? Version { get; set; } 19 | 20 | protected string? QtVersion { get; private set; } 21 | 22 | protected string? LibtorrentVersion { get; private set; } 23 | 24 | protected string? BoostVersion { get; private set; } 25 | 26 | protected string? OpensslVersion { get; private set; } 27 | 28 | protected string? ZlibVersion { get; private set; } 29 | 30 | protected string? QBittorrentVersion { get; private set; } 31 | 32 | protected void NavigateBack() 33 | { 34 | NavigationManager.NavigateToHome(); 35 | } 36 | 37 | protected override async Task OnInitializedAsync() 38 | { 39 | var info = await ApiClient.GetBuildInfo(); 40 | if (Version is null) 41 | { 42 | Version = await ApiClient.GetApplicationVersion(); 43 | } 44 | 45 | QtVersion = info.QTVersion; 46 | LibtorrentVersion = info.LibTorrentVersion; 47 | BoostVersion = info.BoostVersion; 48 | OpensslVersion = info.OpenSSLVersion; 49 | ZlibVersion = info.ZLibVersion; 50 | QBittorrentVersion = $"{Version} ({info.Bitness}-bit)"; 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/SearchResult.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record SearchResult 6 | { 7 | [JsonConstructor] 8 | public SearchResult( 9 | string descriptionLink, 10 | string fileName, 11 | long fileSize, 12 | string fileUrl, 13 | int leechers, 14 | int seeders, 15 | string siteUrl, 16 | string engineName, 17 | long? publishedOn) 18 | { 19 | DescriptionLink = descriptionLink; 20 | FileName = fileName; 21 | FileSize = fileSize; 22 | FileUrl = fileUrl; 23 | Leechers = leechers; 24 | Seeders = seeders; 25 | SiteUrl = siteUrl; 26 | EngineName = engineName; 27 | PublishedOn = publishedOn; 28 | } 29 | 30 | [JsonPropertyName("descrLink")] 31 | public string DescriptionLink { get; set; } 32 | 33 | [JsonPropertyName("fileName")] 34 | public string FileName { get; set; } 35 | 36 | [JsonPropertyName("fileSize")] 37 | public long FileSize { get; set; } 38 | 39 | [JsonPropertyName("fileUrl")] 40 | public string FileUrl { get; set; } 41 | 42 | [JsonPropertyName("nbLeechers")] 43 | public int Leechers { get; set; } 44 | 45 | [JsonPropertyName("nbSeeders")] 46 | public int Seeders { get; set; } 47 | 48 | [JsonPropertyName("siteUrl")] 49 | public string SiteUrl { get; set; } 50 | 51 | [JsonPropertyName("engineName")] 52 | public string EngineName { get; set; } 53 | 54 | [JsonPropertyName("pubDate")] 55 | public long? PublishedOn { get; set; } 56 | } 57 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/FormUrlEncodedBuilder.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Lantean.QBitTorrentClient 4 | { 5 | public class FormUrlEncodedBuilder 6 | { 7 | private readonly IList> _parameters; 8 | 9 | public FormUrlEncodedBuilder() 10 | { 11 | _parameters = []; 12 | } 13 | 14 | public FormUrlEncodedBuilder(IList> parameters) 15 | { 16 | _parameters = parameters; 17 | } 18 | 19 | public FormUrlEncodedBuilder Add(string key, string value) 20 | { 21 | _parameters.Add(new KeyValuePair(key, value)); 22 | return this; 23 | } 24 | 25 | public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, string? value) 26 | { 27 | if (!string.IsNullOrEmpty(value)) 28 | { 29 | _parameters.Add(new KeyValuePair(key, value)); 30 | } 31 | 32 | return this; 33 | } 34 | 35 | public FormUrlEncodedBuilder AddIfNotNullOrEmpty(string key, T? value) where T : struct 36 | { 37 | if (value.HasValue) 38 | { 39 | var stringValue = Convert.ToString(value.Value, CultureInfo.InvariantCulture) ?? string.Empty; 40 | _parameters.Add(new KeyValuePair(key, stringValue)); 41 | } 42 | 43 | return this; 44 | } 45 | 46 | public FormUrlEncodedContent ToFormUrlEncodedContent() 47 | { 48 | return new FormUrlEncodedContent(_parameters); 49 | } 50 | 51 | internal IList> GetParameters() 52 | { 53 | return _parameters; 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Rss.razor.css: -------------------------------------------------------------------------------- 1 | ::deep .rss-article-item { 2 | border-radius: 4px; 3 | } 4 | 5 | ::deep .rss-article-item .mud-list-item-icon { 6 | min-width: 36px !important; 7 | } 8 | 9 | ::deep .rss-article-item--selected { 10 | background-color: var(--mud-palette-primary); 11 | color: var(--mud-palette-primary-text); 12 | } 13 | 14 | ::deep .rss-article-item--selected .mud-list-item-text { 15 | color: inherit; 16 | } 17 | 18 | ::deep .rss-article-item--selected .mud-icon-root { 19 | color: inherit; 20 | } 21 | 22 | .rss-back-button { 23 | margin-bottom: 8px; 24 | } 25 | 26 | .rss-back-button-container { 27 | position: absolute; 28 | top: 0; 29 | left: 0; 30 | right: 0; 31 | z-index: 2; 32 | padding: 8px 12px; 33 | background-color: var(--mud-palette-background); 34 | } 35 | 36 | .rss-slider { 37 | overflow: hidden; 38 | width: 100%; 39 | height: 100%; 40 | background-color: inherit; 41 | } 42 | 43 | .rss-slider__track { 44 | display: flex; 45 | height: 100%; 46 | transition: transform 600ms ease; 47 | background-color: inherit; 48 | } 49 | 50 | .rss-slider__track--one .rss-slider__pane { 51 | flex: 0 0 100%; 52 | } 53 | 54 | .rss-slider__track--two .rss-slider__pane { 55 | flex: 0 0 50%; 56 | } 57 | 58 | .rss-slider__pane { 59 | height: 100%; 60 | box-sizing: border-box; 61 | background-color: inherit; 62 | position: relative; 63 | } 64 | 65 | .rss-slider__pane--articles { 66 | overflow: auto; 67 | } 68 | 69 | .rss-slider__pane--no-scroll { 70 | overflow: hidden !important; 71 | } 72 | 73 | .rss-slider__pane-content { 74 | padding: 0; 75 | height: 100%; 76 | box-sizing: border-box; 77 | background-color: inherit; 78 | } 79 | 80 | .rss-slider__pane-content--with-back { 81 | padding-top: 48px; 82 | } 83 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/MainData.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBTMud.Models 2 | { 3 | public record MainData 4 | { 5 | public MainData( 6 | IDictionary torrents, 7 | IEnumerable tags, 8 | IDictionary categories, 9 | IDictionary> trackers, 10 | ServerState serverState, 11 | Dictionary> tagState, 12 | Dictionary> categoriesState, 13 | Dictionary> statusState, 14 | Dictionary> trackersState) 15 | { 16 | Torrents = torrents.ToDictionary(); 17 | Tags = tags.ToHashSet(); 18 | Categories = categories.ToDictionary(); 19 | Trackers = trackers.ToDictionary(); 20 | ServerState = serverState; 21 | TagState = tagState; 22 | CategoriesState = categoriesState; 23 | StatusState = statusState; 24 | TrackersState = trackersState; 25 | } 26 | 27 | public Dictionary Torrents { get; } 28 | public HashSet Tags { get; } 29 | public Dictionary Categories { get; } 30 | public Dictionary> Trackers { get; } 31 | public ServerState ServerState { get; } 32 | 33 | public Dictionary> TagState { get; } 34 | public Dictionary> CategoriesState { get; } 35 | public Dictionary> StatusState { get; } 36 | public Dictionary> TrackersState { get; } 37 | public string? SelectedTorrentHash { get; set; } 38 | public bool LostConnection { get; set; } 39 | } 40 | } -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/RssArticle.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public class RssArticle 6 | { 7 | [JsonConstructor] 8 | public RssArticle( 9 | string? category, 10 | string? comments, 11 | string? date, 12 | string? description, 13 | string? id, 14 | string? link, 15 | string? thumbnail, 16 | string? title, 17 | string? torrentURL, 18 | bool isRead) 19 | { 20 | Category = category; 21 | Comments = comments; 22 | Date = date; 23 | Description = description; 24 | Id = id; 25 | Link = link; 26 | Thumbnail = thumbnail; 27 | Title = title; 28 | TorrentURL = torrentURL; 29 | IsRead = isRead; 30 | } 31 | 32 | [JsonPropertyName("category")] 33 | public string? Category { get; } 34 | 35 | [JsonPropertyName("comments")] 36 | public string? Comments { get; } 37 | 38 | [JsonPropertyName("date")] 39 | public string? Date { get; } 40 | 41 | [JsonPropertyName("description")] 42 | public string? Description { get; } 43 | 44 | [JsonPropertyName("id")] 45 | public string? Id { get; } 46 | 47 | [JsonPropertyName("link")] 48 | public string? Link { get; } 49 | 50 | [JsonPropertyName("thumbnail")] 51 | public string? Thumbnail { get; } 52 | 53 | [JsonPropertyName("title")] 54 | public string? Title { get; } 55 | 56 | [JsonPropertyName("torrentURL")] 57 | public string? TorrentURL { get; } 58 | 59 | [JsonPropertyName("isRead")] 60 | public bool IsRead { get; } 61 | } 62 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/PiecesProgressCanvas.razor: -------------------------------------------------------------------------------- 1 | @using Lantean.QBitTorrentClient.Models 2 | @using MudBlazor 3 | 4 | 5 | 6 | 7 |
8 | 9 |
10 | @LinearSummary 11 | 12 |
13 |
14 |
15 | 16 | 17 | 18 | @if (!HasPieceData) 19 | { 20 | @CanvasEmptyText 21 | } 22 | else if (ColumnsForCurrentBreakpoint == 0) 23 | { 24 | @CanvasHiddenText 25 | } 26 | else 27 | { 28 | 32 | } 33 | 34 | 35 |
36 |
37 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Categories.razor: -------------------------------------------------------------------------------- 1 | @page "/categories" 2 | @layout OtherLayout 3 | 4 |
5 |
6 | 7 | @if (!DrawerOpen) 8 | { 9 | 10 | 11 | } 12 | Categories 13 | 14 | 15 | 16 |
17 | 18 |
19 | 27 |
28 |
29 | 30 | @code { 31 | private RenderFragment> ActionsColumn 32 | { 33 | get 34 | { 35 | return context => __builder => 36 | { 37 | var value = (Category?)context.GetValue(); 38 | 39 | 40 | 41 | 42 | ; 43 | }; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/TorrentParams.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record TorrentParams 6 | { 7 | public TorrentParams() 8 | { 9 | Category = ""; 10 | DownloadPath = ""; 11 | OperatingMode = ""; 12 | SavePath = ""; 13 | Tags = []; 14 | } 15 | 16 | [JsonPropertyName("category")] 17 | public string Category { get; set; } 18 | 19 | [JsonPropertyName("download_limit")] 20 | public int? DownloadLimit { get; set; } 21 | 22 | [JsonPropertyName("download_path")] 23 | public string DownloadPath { get; set; } 24 | 25 | [JsonPropertyName("inactive_seeding_time_limit")] 26 | public int? InactiveSeedingTimeLimit { get; set; } 27 | 28 | [JsonPropertyName("operating_mode")] 29 | public string OperatingMode { get; set; } 30 | 31 | [JsonPropertyName("ratio_limit")] 32 | public int? RatioLimit { get; set; } 33 | 34 | [JsonPropertyName("save_path")] 35 | public string SavePath { get; set; } 36 | 37 | [JsonPropertyName("seeding_time_limit")] 38 | public int? SeedingTimeLimit { get; set; } 39 | 40 | [JsonPropertyName("skip_checking")] 41 | public bool? SkipChecking { get; set; } 42 | 43 | [JsonPropertyName("stopped")] 44 | public bool? Stopped { get; set; } 45 | 46 | [JsonPropertyName("tags")] 47 | public IReadOnlyList Tags { get; set; } 48 | 49 | [JsonPropertyName("upload_limit")] 50 | public int? UploadLimit { get; set; } 51 | 52 | [JsonPropertyName("use_auto_tmm")] 53 | public bool UseAutoTmm { get; set; } 54 | 55 | [JsonPropertyName("content_layout")] 56 | public string? ContentLayout { get; set; } 57 | } 58 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Login.razor: -------------------------------------------------------------------------------- 1 | @page "/login" 2 | @layout MainLayout 3 | 4 | Login 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | Login 14 | 15 | 16 | @if (ApiError is not null) 17 | { 18 | @ApiError 19 | } 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | Login 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/MultipleFieldDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Components.Dialogs 6 | { 7 | public partial class MultipleFieldDialog 8 | { 9 | [CascadingParameter] 10 | private IMudDialogInstance MudDialog { get; set; } = default!; 11 | 12 | [Parameter] 13 | public string Label { get; set; } = default!; 14 | 15 | [Parameter] 16 | public HashSet Values { get; set; } = []; 17 | 18 | protected HashSet NewValues { get; } = []; 19 | 20 | protected string? Value { get; set; } 21 | 22 | protected override void OnParametersSet() 23 | { 24 | if (NewValues.Count == 0) 25 | { 26 | foreach (var value in Values) 27 | { 28 | NewValues.Add(value); 29 | } 30 | } 31 | } 32 | 33 | protected void AddValue() 34 | { 35 | if (string.IsNullOrEmpty(Value)) 36 | { 37 | return; 38 | } 39 | NewValues.Add(Value); 40 | Value = null; 41 | } 42 | 43 | protected void SetValue(string tracker) 44 | { 45 | Value = tracker; 46 | } 47 | 48 | protected void DeleteValue(string tracker) 49 | { 50 | NewValues.Remove(tracker); 51 | } 52 | 53 | protected void Cancel() 54 | { 55 | MudDialog.Cancel(); 56 | } 57 | 58 | protected void Submit() 59 | { 60 | MudDialog.Close(NewValues); 61 | } 62 | 63 | protected override Task Submit(KeyboardEvent keyboardEvent) 64 | { 65 | Submit(); 66 | 67 | return Task.CompletedTask; 68 | } 69 | } 70 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Helpers/ColumnDefinitionHelper.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | 5 | namespace Lantean.QBTMud.Helpers 6 | { 7 | public static class ColumnDefinitionHelper 8 | { 9 | public static ColumnDefinition CreateColumnDefinition(string name, Func selector, RenderFragment> rowTemplate, bool iconOnly = false, int? width = null, string? tdClass = null, Func? classFunc = null, bool enabled = true, SortDirection initialDirection = SortDirection.None) 10 | { 11 | var cd = new ColumnDefinition(name, selector, rowTemplate); 12 | cd.Class = "no-wrap"; 13 | if (tdClass is not null) 14 | { 15 | cd.Class += " " + tdClass; 16 | } 17 | cd.ClassFunc = classFunc; 18 | cd.Width = width; 19 | cd.Enabled = enabled; 20 | cd.InitialDirection = initialDirection; 21 | cd.IconOnly = iconOnly; 22 | return cd; 23 | } 24 | 25 | public static ColumnDefinition CreateColumnDefinition(string name, Func selector, Func? formatter = null, bool iconOnly = false, int? width = null, string? tdClass = null, Func? classFunc = null, bool enabled = true, SortDirection initialDirection = SortDirection.None) 26 | { 27 | var cd = new ColumnDefinition(name, selector, formatter); 28 | cd.Class = "no-wrap"; 29 | if (tdClass is not null) 30 | { 31 | cd.Class += " " + tdClass; 32 | } 33 | cd.ClassFunc = classFunc; 34 | cd.Width = width; 35 | cd.Enabled = enabled; 36 | cd.InitialDirection = initialDirection; 37 | cd.IconOnly = iconOnly; 38 | 39 | return cd; 40 | } 41 | } 42 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/wwwroot/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | qBittorrent Web UI 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 |
23 | 24 | 25 | 26 | 27 |
28 |
29 | 30 |
31 | An unhandled error has occurred. 32 | Reload 33 | 🗙 34 |
35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/PeersTab.razor: -------------------------------------------------------------------------------- 1 | 2 | Add peer 3 | @if (ContextMenuItem is not null) 4 | { 5 | Copy IP:port 6 | Ban peer 7 | } 8 | 9 | 10 |
11 |
12 | 13 | Add peer 14 | Ban peer 15 | 16 | 17 | 18 |
19 | 20 |
21 | 31 |
32 |
33 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Services/RssDataManager.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | 3 | namespace Lantean.QBTMud.Services 4 | { 5 | public class RssDataManager : IRssDataManager 6 | { 7 | public RssList CreateRssList(IReadOnlyDictionary rssItems) 8 | { 9 | var articles = new List(); 10 | var feeds = new Dictionary(StringComparer.Ordinal); 11 | foreach (var (key, rssItem) in rssItems) 12 | { 13 | feeds.Add( 14 | key, 15 | new RssFeed( 16 | rssItem.HasError, 17 | rssItem.IsLoading, 18 | rssItem.LastBuildDate, 19 | rssItem.Title, 20 | rssItem.Uid, 21 | rssItem.Url, 22 | key)); 23 | if (rssItem.Articles is null) 24 | { 25 | continue; 26 | } 27 | foreach (var rssArticle in rssItem.Articles) 28 | { 29 | var article = new RssArticle( 30 | key, 31 | rssArticle.Category, 32 | rssArticle.Comments, 33 | rssArticle.Date ?? string.Empty, 34 | rssArticle.Description, 35 | rssArticle.Id ?? string.Empty, 36 | rssArticle.Link, 37 | rssArticle.Thumbnail, 38 | rssArticle.Title ?? string.Empty, 39 | rssArticle.TorrentURL ?? string.Empty, 40 | rssArticle.IsRead); 41 | 42 | articles.Add(article); 43 | } 44 | } 45 | 46 | return new RssList(feeds, articles); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/AddTorrentParams.cs: -------------------------------------------------------------------------------- 1 | namespace Lantean.QBitTorrentClient.Models 2 | { 3 | public record AddTorrentParams 4 | { 5 | public IEnumerable? Urls { get; set; } 6 | 7 | public bool? SkipChecking { get; set; } 8 | 9 | public bool? SequentialDownload { get; set; } 10 | 11 | public bool? FirstLastPiecePriority { get; set; } 12 | 13 | public bool? AddToTopOfQueue { get; set; } 14 | 15 | public bool? Forced { get; set; } 16 | 17 | public bool? Stopped { get; set; } 18 | 19 | public string? SavePath { get; set; } 20 | 21 | public string? DownloadPath { get; set; } 22 | 23 | public bool? UseDownloadPath { get; set; } 24 | 25 | public string? Category { get; set; } 26 | 27 | public IEnumerable? Tags { get; set; } 28 | 29 | public string? RenameTorrent { get; set; } 30 | 31 | public long? UploadLimit { get; set; } 32 | 33 | public long? DownloadLimit { get; set; } 34 | 35 | public float? RatioLimit { get; set; } 36 | 37 | public int? SeedingTimeLimit { get; set; } 38 | 39 | public int? InactiveSeedingTimeLimit { get; set; } 40 | 41 | public ShareLimitAction? ShareLimitAction { get; set; } 42 | 43 | public bool? AutoTorrentManagement { get; set; } 44 | 45 | public StopCondition? StopCondition { get; set; } 46 | 47 | public TorrentContentLayout? ContentLayout { get; set; } 48 | 49 | public IEnumerable? FilePriorities { get; set; } 50 | 51 | public string? Downloader { get; set; } 52 | 53 | public string? SslCertificate { get; set; } 54 | 55 | public string? SslPrivateKey { get; set; } 56 | 57 | public string? SslDhParams { get; set; } 58 | 59 | public string? Cookie { get; set; } 60 | 61 | public Dictionary? Torrents { get; set; } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/QueryBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Globalization; 2 | 3 | namespace Lantean.QBitTorrentClient 4 | { 5 | public static class QueryBuilderExtensions 6 | { 7 | public static QueryBuilder Add(this QueryBuilder builder, string key, bool value) 8 | { 9 | return builder.Add(key, value ? "true" : "false"); 10 | } 11 | 12 | public static QueryBuilder Add(this QueryBuilder builder, string key, int value) 13 | { 14 | return builder.Add(key, value.ToString(CultureInfo.InvariantCulture)); 15 | } 16 | 17 | public static QueryBuilder Add(this QueryBuilder builder, string key, long value) 18 | { 19 | return builder.Add(key, value.ToString(CultureInfo.InvariantCulture)); 20 | } 21 | 22 | public static QueryBuilder Add(this QueryBuilder builder, string key, DateTimeOffset value, bool useSeconds = true) 23 | { 24 | return builder.Add(key, useSeconds ? value.ToUnixTimeSeconds() : value.ToUnixTimeMilliseconds()); 25 | } 26 | 27 | public static QueryBuilder Add(this QueryBuilder builder, string key, Enum value) 28 | { 29 | return builder.Add(key, value.ToString()); 30 | } 31 | 32 | public static QueryBuilder AddPipeSeparated(this QueryBuilder builder, string key, IEnumerable values) 33 | { 34 | return builder.Add(key, JoinWithInvariant(values, '|')); 35 | } 36 | 37 | public static QueryBuilder AddCommaSeparated(this QueryBuilder builder, string key, IEnumerable values) 38 | { 39 | return builder.Add(key, JoinWithInvariant(values, ',')); 40 | } 41 | 42 | private static string JoinWithInvariant(IEnumerable values, char separator) 43 | { 44 | return string.Join(separator, values.Select(value => Convert.ToString(value, CultureInfo.InvariantCulture) ?? string.Empty)); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Models/SearchJobViewModelTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBitTorrentClient.Models; 3 | using Lantean.QBTMud.Models; 4 | 5 | namespace Lantean.QBTMud.Test.Models 6 | { 7 | public sealed class SearchJobViewModelTests 8 | { 9 | [Fact] 10 | public void GIVEN_SearchJob_WHEN_UpdateStatusToCompleted_THEN_CompletedOnSet() 11 | { 12 | var job = new SearchJobViewModel(1, "Ubuntu", new[] { "movies" }, SearchForm.AllCategoryId); 13 | 14 | job.AppendResults(new[] 15 | { 16 | new SearchResult("http://desc", "Ubuntu", 1_000_000, "http://files", 1, 10, "http://site", "movies", 1_700_000_000) 17 | }); 18 | 19 | job.UpdateStatus("Completed", 1); 20 | 21 | job.Status.Should().Be("Completed"); 22 | job.CompletedOn.Should().NotBeNull(); 23 | job.CurrentOffset.Should().Be(1); 24 | 25 | job.SetError("failed"); 26 | job.Status.Should().Be("Error"); 27 | job.ErrorMessage.Should().Be("failed"); 28 | 29 | job.ResetResults(clearError: false); 30 | job.Results.Should().BeEmpty(); 31 | job.ErrorMessage.Should().Be("failed"); 32 | 33 | job.ResetResults(); 34 | job.ErrorMessage.Should().BeNull(); 35 | } 36 | 37 | [Fact] 38 | public void GIVEN_SearchJob_WHEN_MatchesCalled_THEN_ComparesAllCriteria() 39 | { 40 | var job = new SearchJobViewModel(2, "Ubuntu", new[] { "movies", "tv" }, SearchForm.AllCategoryId); 41 | 42 | job.Matches("Ubuntu", SearchForm.AllCategoryId, new[] { "movies", "tv" }).Should().BeTrue(); 43 | job.Matches("Ubuntu", SearchForm.AllCategoryId, new[] { "movies" }).Should().BeFalse(); 44 | job.Matches("Fedora", SearchForm.AllCategoryId, new[] { "movies", "tv" }).Should().BeFalse(); 45 | job.Matches("Ubuntu", "movies", new[] { "movies", "tv" }).Should().BeFalse(); 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/ColumnDefinition.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Components; 2 | using MudBlazor; 3 | 4 | namespace Lantean.QBTMud.Models 5 | { 6 | public class ColumnDefinition 7 | { 8 | public ColumnDefinition(string header, Func sortSelector, Func? formatter = null, string? tdClass = null, int? width = null) 9 | { 10 | Header = header; 11 | SortSelector = sortSelector; 12 | Formatter = formatter; 13 | Class = tdClass; 14 | Width = width; 15 | 16 | RowTemplate = (context) => (builder) => builder.AddContent(1, context.GetValue()); 17 | } 18 | 19 | public ColumnDefinition(string header, Func sortSelector, RenderFragment> rowTemplate, Func? formatter = null, string? tdClass = null, int? width = null) 20 | { 21 | Header = header; 22 | SortSelector = sortSelector; 23 | RowTemplate = rowTemplate; 24 | Formatter = formatter; 25 | Class = tdClass; 26 | Width = width; 27 | } 28 | 29 | public string Id => Header.ToLowerInvariant().Replace(' ', '_'); 30 | 31 | public string Header { get; set; } 32 | 33 | public Func SortSelector { get; set; } 34 | 35 | public RenderFragment> RowTemplate { get; set; } 36 | 37 | public bool IconOnly { get; set; } 38 | 39 | public int? Width { get; set; } 40 | 41 | public Func? Formatter { get; set; } 42 | 43 | public string? Class { get; set; } 44 | 45 | public Func? ClassFunc { get; set; } 46 | 47 | public bool Enabled { get; set; } = true; 48 | 49 | public SortDirection InitialDirection { get; set; } = SortDirection.None; 50 | 51 | public RowContext GetRowContext(T data) 52 | { 53 | return new RowContext(Header, data, Formatter is null ? SortSelector : Formatter); 54 | } 55 | } 56 | } -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Helpers/SearchFilterHelperTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Lantean.QBitTorrentClient.Models; 3 | using Lantean.QBTMud.Helpers; 4 | using Lantean.QBTMud.Models; 5 | 6 | namespace Lantean.QBTMud.Test.Helpers 7 | { 8 | public sealed class SearchFilterHelperTests 9 | { 10 | [Fact] 11 | public void GIVEN_SizeFilterBelowZero_WHEN_Convert_THEN_ReturnsZero() 12 | { 13 | var options = new SearchFilterOptions(null, SearchInScope.Everywhere, null, null, -1, SearchSizeUnit.Mebibytes, null, SearchSizeUnit.Gibibytes); 14 | var count = SearchFilterHelper.CountVisible(Array.Empty(), options); 15 | count.Should().Be(0); 16 | } 17 | 18 | [Fact] 19 | public void GIVEN_ExtremelyLargeSize_WHEN_Filter_THEN_ValueClamped() 20 | { 21 | var result = new SearchResult("http://desc", "Huge", long.MaxValue, "http://files", 1, 1, "http://site", "movies", null); 22 | var options = new SearchFilterOptions(null, SearchInScope.Everywhere, null, null, null, SearchSizeUnit.Bytes, 9.22e18, SearchSizeUnit.Gibibytes); 23 | 24 | var visible = SearchFilterHelper.ApplyFilters(new[] { result }, options); 25 | visible.Should().ContainSingle(); 26 | } 27 | 28 | [Fact] 29 | public void GIVEN_FilterTextEverywhere_WHEN_SearchableFieldsEvaluated_THEN_ResultMatched() 30 | { 31 | var result = new SearchResult("http://desc", "Filtered Name", 1_000, "http://files", 1, 1, "http://site", "movies", null); 32 | var options = new SearchFilterOptions("site", SearchInScope.Everywhere, null, null, null, SearchSizeUnit.Bytes, null, SearchSizeUnit.Bytes); 33 | 34 | var filtered = SearchFilterHelper.ApplyFilters(new[] { result }, options); 35 | filtered.Should().ContainSingle(); 36 | 37 | var missing = SearchFilterHelper.ApplyFilters(new[] { result }, options with { FilterText = "missing" }); 38 | missing.Should().BeEmpty(); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Components/UI/TdExtendedTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Bunit; 3 | using Lantean.QBTMud.Components.UI; 4 | using Lantean.QBTMud.Test.Infrastructure; 5 | using Microsoft.AspNetCore.Components; 6 | using Microsoft.AspNetCore.Components.Web; 7 | 8 | namespace Lantean.QBTMud.Test.Components.UI 9 | { 10 | public sealed class TdExtendedTests : RazorComponentTestBase 11 | { 12 | [Fact] 13 | public async Task GIVEN_LongPressHandler_WHEN_LongPressRaised_THEN_ShouldInvokeCallback() 14 | { 15 | var invoked = false; 16 | 17 | var target = TestContext.Render(parameters => 18 | { 19 | parameters.Add(p => p.OnLongPress, EventCallback.Factory.Create(this, _ => 20 | { 21 | invoked = true; 22 | return Task.CompletedTask; 23 | })); 24 | parameters.Add(p => p.ChildContent, builder => builder.AddContent(0, "ChildContent")); 25 | }); 26 | 27 | await target.Find("td").TriggerEventAsync("onlongpress", new LongPressEventArgs()); 28 | 29 | invoked.Should().BeTrue(); 30 | } 31 | 32 | [Fact] 33 | public async Task GIVENTestContextMenuHandler_WHENTestContextMenuRaised_THEN_ShouldInvokeCallback() 34 | { 35 | var invoked = false; 36 | 37 | var target = TestContext.Render(parameters => 38 | { 39 | parameters.Add(p => p.OnContextMenu, EventCallback.Factory.Create(this, _ => 40 | { 41 | invoked = true; 42 | return Task.CompletedTask; 43 | })); 44 | parameters.Add(p => p.ChildContent, builder => builder.AddContent(0, "ChildContent")); 45 | }); 46 | 47 | await target.Find("td").TriggerEventAsync("oncontextmenu", new MouseEventArgs()); 48 | 49 | invoked.Should().BeTrue(); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Components/UI/FieldSwitchTests.cs: -------------------------------------------------------------------------------- 1 | using AwesomeAssertions; 2 | using Bunit; 3 | using Lantean.QBTMud.Components.UI; 4 | using Lantean.QBTMud.Test.Infrastructure; 5 | using Microsoft.AspNetCore.Components; 6 | 7 | namespace Lantean.QBTMud.Test.Components.UI 8 | { 9 | public sealed class FieldSwitchTests : RazorComponentTestBase 10 | { 11 | [Fact] 12 | public void GIVEN_LabelAndHelper_WHEN_Rendered_THEN_ShouldRenderFieldAndSwitch() 13 | { 14 | var target = TestContext.Render(parameters => 15 | { 16 | parameters.Add(p => p.Label, "Label"); 17 | parameters.Add(p => p.HelperText, "HelperText"); 18 | parameters.Add(p => p.Value, false); 19 | }); 20 | 21 | target.Markup.Should().Contain("Label"); 22 | target.Markup.Should().Contain("HelperText"); 23 | } 24 | 25 | [Fact] 26 | public void GIVEN_DisabledSwitch_WHEN_Rendered_THEN_ShouldDisableInput() 27 | { 28 | var target = TestContext.Render(parameters => 29 | { 30 | parameters.Add(p => p.Disabled, true); 31 | parameters.Add(p => p.Value, false); 32 | }); 33 | 34 | var input = target.Find("input"); 35 | input.HasAttribute("disabled").Should().BeTrue(); 36 | } 37 | 38 | [Fact] 39 | public void GIVEN_ValueChanged_WHEN_Toggled_THEN_ShouldUpdateValueAndInvokeCallback() 40 | { 41 | var callbackValue = false; 42 | 43 | var target = TestContext.Render(parameters => 44 | { 45 | parameters.Add(p => p.Value, false); 46 | parameters.Add(p => p.ValueChanged, EventCallback.Factory.Create(this, value => callbackValue = value)); 47 | }); 48 | 49 | target.Find("input").Change(true); 50 | 51 | callbackValue.Should().BeTrue(); 52 | target.Instance.Value.Should().BeTrue(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/NumericFieldDialog.razor.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBTMud.Models; 2 | using Microsoft.AspNetCore.Components; 3 | using MudBlazor; 4 | using System.Numerics; 5 | 6 | namespace Lantean.QBTMud.Components.Dialogs 7 | { 8 | public partial class NumericFieldDialog where T : struct, INumber 9 | { 10 | [CascadingParameter] 11 | private IMudDialogInstance MudDialog { get; set; } = default!; 12 | 13 | [Parameter] 14 | public string? Label { get; set; } 15 | 16 | [Parameter] 17 | public T Value { get; set; } 18 | 19 | [Parameter] 20 | public T Min { get; set; } = T.Zero; 21 | 22 | [Parameter] 23 | public T Max { get; set; } = T.One; 24 | 25 | [Parameter] 26 | public bool Disabled { get; set; } 27 | 28 | [Parameter] 29 | public Func? ValueDisplayFunc { get; set; } 30 | 31 | [Parameter] 32 | public Func? ValueGetFunc { get; set; } 33 | 34 | private string? GetDisplayValue() 35 | { 36 | var value = ValueDisplayFunc?.Invoke(Value); 37 | return value is null ? Value.ToString() : value; 38 | } 39 | 40 | protected void ValueChanged(string value) 41 | { 42 | if (ValueGetFunc is not null) 43 | { 44 | Value = ValueGetFunc.Invoke(value); 45 | 46 | return; 47 | } 48 | 49 | if (T.TryParse(value, null, out var result)) 50 | { 51 | Value = result; 52 | } 53 | else 54 | { 55 | Value = Min; 56 | } 57 | } 58 | 59 | protected void Cancel() 60 | { 61 | MudDialog.Cancel(); 62 | } 63 | 64 | protected void Submit() 65 | { 66 | MudDialog.Close(DialogResult.Ok(Value)); 67 | } 68 | 69 | protected override Task Submit(KeyboardEvent keyboardEvent) 70 | { 71 | Submit(); 72 | 73 | return Task.CompletedTask; 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Options/Options.cs: -------------------------------------------------------------------------------- 1 | using Lantean.QBitTorrentClient.Models; 2 | using Microsoft.AspNetCore.Components; 3 | 4 | namespace Lantean.QBTMud.Components.Options 5 | { 6 | public abstract class Options : ComponentBase 7 | { 8 | private bool _preferencesRead; 9 | 10 | protected const int MinPortValue = 1024; 11 | protected const int MinNonNegativePortValue = 0; 12 | protected const int MaxPortValue = 65535; 13 | 14 | [Parameter] 15 | [EditorRequired] 16 | public Preferences? Preferences { get; set; } 17 | 18 | [Parameter] 19 | [EditorRequired] 20 | public UpdatePreferences UpdatePreferences { get; set; } = default!; 21 | 22 | [Parameter] 23 | [EditorRequired] 24 | public EventCallback PreferencesChanged { get; set; } 25 | 26 | protected Func PortNonNegativeValidation = (port) => 27 | { 28 | if (port < MinNonNegativePortValue || port > MaxPortValue) 29 | { 30 | return $"The port used for incoming connections must be between {MinNonNegativePortValue} and {MaxPortValue}."; 31 | } 32 | 33 | return null; 34 | }; 35 | 36 | protected Func PortValidation = (port) => 37 | { 38 | if (port < MinPortValue || port > MaxPortValue) 39 | { 40 | return $"The port used for incoming connections must be between {MinPortValue} and {MaxPortValue}."; 41 | } 42 | 43 | return null; 44 | }; 45 | 46 | public async Task ResetAsync() 47 | { 48 | SetOptions(); 49 | 50 | await InvokeAsync(StateHasChanged); 51 | } 52 | 53 | protected override void OnParametersSet() 54 | { 55 | UpdatePreferences ??= new UpdatePreferences(); 56 | 57 | if (_preferencesRead) 58 | { 59 | return; 60 | } 61 | 62 | _preferencesRead = SetOptions(); 63 | } 64 | 65 | protected abstract bool SetOptions(); 66 | } 67 | } -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Models/ContentItem.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | namespace Lantean.QBTMud.Models 4 | { 5 | [DebuggerDisplay("{Name}")] 6 | public class ContentItem 7 | { 8 | public ContentItem( 9 | string name, 10 | string displayName, 11 | int index, 12 | Priority priority, 13 | float progress, 14 | long size, 15 | float availability, 16 | bool isFolder = false, 17 | int level = 0, 18 | long? downloadSize = null) 19 | { 20 | Name = name; 21 | DisplayName = displayName; 22 | Index = index; 23 | Priority = priority; 24 | Progress = progress; 25 | Size = size; 26 | DownloadSize = downloadSize ?? size; 27 | Availability = availability; 28 | IsFolder = isFolder; 29 | Level = level; 30 | } 31 | 32 | public string Name { get; } 33 | 34 | public string Path => IsFolder ? Name : Name.GetDirectoryPath(); 35 | 36 | public string DisplayName { get; } 37 | 38 | public int Index { get; } 39 | 40 | public Priority Priority { get; set; } 41 | 42 | public float Progress { get; set; } 43 | 44 | public long Size { get; set; } 45 | 46 | public long DownloadSize { get; set; } 47 | 48 | public float Availability { get; set; } 49 | 50 | public long Downloaded => (long)Math.Round(DownloadSize * (double)Progress, 0); 51 | 52 | public long Remaining => Progress == 1 || Priority == Priority.DoNotDownload ? 0 : DownloadSize - Downloaded; 53 | 54 | public bool IsFolder { get; } 55 | 56 | public int Level { get; } 57 | 58 | public override bool Equals(object? obj) 59 | { 60 | if (obj is null) 61 | { 62 | return false; 63 | } 64 | 65 | return ((ContentItem)obj).Name == Name; 66 | } 67 | 68 | public override int GetHashCode() 69 | { 70 | return Name.GetHashCode(); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Components/Dialogs/ColumnOptionsDialog.razor: -------------------------------------------------------------------------------- 1 | @typeparam T 2 | @inherits SubmittableDialog 3 | @using Lantean.QBTMud.Helpers 4 | 5 | 6 | 7 | 8 | 9 | @for (var i = 0; i < OrderedColumns.Length; i++) 10 | { 11 | var item = OrderedColumns[i]; 12 | var column = Columns.First(c => c.Id == item); 13 | var index = i; 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | } 27 | 28 | 29 | 30 | 31 | Cancel 32 | Save 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Pages/Details.razor: -------------------------------------------------------------------------------- 1 | @page "/details/{hash}" 2 | @layout DetailsLayout 3 | 4 |
5 |
6 | 7 | @if (!DrawerOpen) 8 | { 9 | 10 | 11 | } 12 | @if (Hash is not null) 13 | { 14 | 15 | } 16 | 17 | @Name 18 | 19 |
20 | 21 |
22 | @if (ShowTabs) 23 | { 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | } 44 |
45 |
46 | -------------------------------------------------------------------------------- /test/Lantean.QBTMud.Test/Services/CookieHandlerTests.cs: -------------------------------------------------------------------------------- 1 | using System.Net; 2 | using AwesomeAssertions; 3 | using Lantean.QBTMud.Services; 4 | 5 | namespace Lantean.QBTMud.Test.Services 6 | { 7 | public class CookieHandlerTests 8 | { 9 | private readonly CookieHandler _target; 10 | 11 | public CookieHandlerTests() 12 | { 13 | _target = new CookieHandler(); 14 | } 15 | 16 | [Fact] 17 | public async Task GIVEN_Request_WHEN_SendAsync_THEN_ShouldIncludeBrowserCredentials() 18 | { 19 | var inner = new SpyHandler(); 20 | _target.InnerHandler = inner; 21 | using var invoker = new HttpMessageInvoker(_target, disposeHandler: false); 22 | var request = new HttpRequestMessage(HttpMethod.Get, "http://example.com"); 23 | 24 | var response = await invoker.SendAsync(request, CancellationToken.None); 25 | 26 | inner.SentRequests.Count.Should().Be(1); 27 | inner.CredentialsIncluded.Should().BeTrue(); 28 | response.Should().BeSameAs(inner.Response); 29 | } 30 | 31 | private sealed class SpyHandler : HttpMessageHandler 32 | { 33 | public List SentRequests { get; } = new(); 34 | 35 | public HttpResponseMessage Response { get; } = new(HttpStatusCode.OK); 36 | 37 | public bool CredentialsIncluded { get; private set; } 38 | 39 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 40 | { 41 | SentRequests.Add(request); 42 | foreach (var option in request.Options) 43 | { 44 | if (option.Value is Dictionary fetchOptions && 45 | fetchOptions.TryGetValue("credentials", out var value) && 46 | value is string credentials && 47 | credentials == "include") 48 | { 49 | CredentialsIncluded = true; 50 | } 51 | } 52 | 53 | return Task.FromResult(Response); 54 | } 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Lantean.QBTMud/Layout/MainLayout.razor: -------------------------------------------------------------------------------- 1 | @inherits LayoutComponentBase 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | qBittorrent Web UI 13 | 14 | 15 | 16 | 17 | @AppBarTitle 18 | 19 | @if (ErrorBoundary?.Errors.Count > 0) 20 | { 21 | 22 | 23 | 24 | } 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | @Body 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/Lantean.QBitTorrentClient/Models/GlobalTransferInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace Lantean.QBitTorrentClient.Models 4 | { 5 | public record GlobalTransferInfo 6 | { 7 | [JsonConstructor] 8 | public GlobalTransferInfo( 9 | string? connectionStatus, 10 | int? dHTNodes, 11 | long? downloadInfoData, 12 | long? downloadInfoSpeed, 13 | long? downloadRateLimit, 14 | long? uploadInfoData, 15 | long? uploadInfoSpeed, 16 | long? uploadRateLimit, 17 | string? lastExternalAddressV4 = null, 18 | string? lastExternalAddressV6 = null) 19 | { 20 | ConnectionStatus = connectionStatus; 21 | DHTNodes = dHTNodes; 22 | DownloadInfoData = downloadInfoData; 23 | DownloadInfoSpeed = downloadInfoSpeed; 24 | DownloadRateLimit = downloadRateLimit; 25 | UploadInfoData = uploadInfoData; 26 | UploadInfoSpeed = uploadInfoSpeed; 27 | UploadRateLimit = uploadRateLimit; 28 | LastExternalAddressV4 = lastExternalAddressV4; 29 | LastExternalAddressV6 = lastExternalAddressV6; 30 | } 31 | 32 | [JsonPropertyName("connection_status")] 33 | public string? ConnectionStatus { get; } 34 | 35 | [JsonPropertyName("dht_nodes")] 36 | public int? DHTNodes { get; } 37 | 38 | [JsonPropertyName("dl_info_data")] 39 | public long? DownloadInfoData { get; } 40 | 41 | [JsonPropertyName("dl_info_speed")] 42 | public long? DownloadInfoSpeed { get; } 43 | 44 | [JsonPropertyName("dl_rate_limit")] 45 | public long? DownloadRateLimit { get; } 46 | 47 | [JsonPropertyName("up_info_data")] 48 | public long? UploadInfoData { get; } 49 | 50 | [JsonPropertyName("up_info_speed")] 51 | public long? UploadInfoSpeed { get; } 52 | 53 | [JsonPropertyName("up_rate_limit")] 54 | public long? UploadRateLimit { get; } 55 | 56 | [JsonPropertyName("last_external_address_v4")] 57 | public string? LastExternalAddressV4 { get; } 58 | 59 | [JsonPropertyName("last_external_address_v6")] 60 | public string? LastExternalAddressV6 { get; } 61 | } 62 | } 63 | --------------------------------------------------------------------------------