Compare commits
4 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ce10c5dfb2 | ||
|
|
129a4c7586 | ||
|
|
f2dfa7963e | ||
|
|
d3792e78e0 |
143
Cargo.lock
generated
143
Cargo.lock
generated
@@ -310,6 +310,7 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"strum",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -544,7 +545,7 @@ version = "4.5.55"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"heck",
|
"heck 0.5.0",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.114",
|
"syn 2.0.114",
|
||||||
@@ -566,6 +567,15 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "codee"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5d3ad3122b0001c7f140cf4d605ef9a9e2c24d96ab0b4fb4347b76de2425f445"
|
||||||
|
dependencies = [
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "collection_literals"
|
name = "collection_literals"
|
||||||
version = "1.0.3"
|
version = "1.0.3"
|
||||||
@@ -790,6 +800,41 @@ version = "1.1.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
checksum = "9b10589d1a5e400d61f9f38f12f884cfd080ff345de8f17efda36fe0e4a02aa8"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"darling_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_core"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
|
||||||
|
dependencies = [
|
||||||
|
"fnv",
|
||||||
|
"ident_case",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"strsim",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "darling_macro"
|
||||||
|
version = "0.20.11"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
|
||||||
|
dependencies = [
|
||||||
|
"darling_core",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dashmap"
|
name = "dashmap"
|
||||||
version = "5.5.3"
|
version = "5.5.3"
|
||||||
@@ -823,6 +868,18 @@ version = "2.10.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "default-struct-builder"
|
||||||
|
version = "0.5.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "e0df63c21a4383f94bd5388564829423f35c316aed85dc4f8427aded372c7c0d"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "der"
|
name = "der"
|
||||||
version = "0.4.5"
|
version = "0.4.5"
|
||||||
@@ -1144,7 +1201,9 @@ dependencies = [
|
|||||||
name = "frontend"
|
name = "frontend"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
"chrono",
|
"chrono",
|
||||||
|
"codee",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"console_log",
|
"console_log",
|
||||||
"futures",
|
"futures",
|
||||||
@@ -1152,9 +1211,11 @@ dependencies = [
|
|||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-use",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
"tailwind_fuse",
|
"tailwind_fuse",
|
||||||
@@ -1476,6 +1537,12 @@ dependencies = [
|
|||||||
"hashbrown 0.15.5",
|
"hashbrown 0.15.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "heck"
|
||||||
|
version = "0.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
@@ -1819,6 +1886,12 @@ dependencies = [
|
|||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ident_case"
|
||||||
|
version = "1.0.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
@@ -1986,6 +2059,30 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-use"
|
||||||
|
version = "0.13.13"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "789bf9f4337e6ebd8f1b407e3f762fdc538d48dc145f9d1dce2338014b38f4dd"
|
||||||
|
dependencies = [
|
||||||
|
"cfg-if",
|
||||||
|
"chrono",
|
||||||
|
"codee",
|
||||||
|
"cookie",
|
||||||
|
"default-struct-builder",
|
||||||
|
"futures-util",
|
||||||
|
"gloo-timers",
|
||||||
|
"js-sys",
|
||||||
|
"lazy_static",
|
||||||
|
"leptos",
|
||||||
|
"paste",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
"unic-langid",
|
||||||
|
"wasm-bindgen",
|
||||||
|
"wasm-bindgen-futures",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos_config"
|
name = "leptos_config"
|
||||||
version = "0.6.15"
|
version = "0.6.15"
|
||||||
@@ -3613,7 +3710,7 @@ checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"dotenvy",
|
"dotenvy",
|
||||||
"either",
|
"either",
|
||||||
"heck",
|
"heck 0.5.0",
|
||||||
"hex",
|
"hex",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
@@ -3756,6 +3853,28 @@ version = "0.11.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum"
|
||||||
|
version = "0.25.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125"
|
||||||
|
dependencies = [
|
||||||
|
"strum_macros",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "strum_macros"
|
||||||
|
version = "0.25.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0"
|
||||||
|
dependencies = [
|
||||||
|
"heck 0.4.1",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"rustversion",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -3934,6 +4053,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"displaydoc",
|
"displaydoc",
|
||||||
|
"serde_core",
|
||||||
"zerovec",
|
"zerovec",
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -4298,6 +4418,24 @@ version = "1.19.0"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a28ba52c9b05311f4f6e62d5d9d46f094bd6e84cb8df7b3ef952748d752a7d05"
|
||||||
|
dependencies = [
|
||||||
|
"unic-langid-impl",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unic-langid-impl"
|
||||||
|
version = "0.9.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dce1bf08044d4b7a94028c93786f8566047edc11110595914de93362559bc658"
|
||||||
|
dependencies = [
|
||||||
|
"tinystr",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicase"
|
name = "unicase"
|
||||||
version = "2.9.0"
|
version = "2.9.0"
|
||||||
@@ -5055,6 +5193,7 @@ version = "0.11.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"serde",
|
||||||
"yoke",
|
"yoke",
|
||||||
"zerofrom",
|
"zerofrom",
|
||||||
"zerovec-derive",
|
"zerovec-derive",
|
||||||
|
|||||||
@@ -42,3 +42,4 @@ anyhow = "1.0.101"
|
|||||||
time = { version = "0.3.47", features = ["serde", "formatting", "parsing"] }
|
time = { version = "0.3.47", features = ["serde", "formatting", "parsing"] }
|
||||||
tower_governor = "0.8.0"
|
tower_governor = "0.8.0"
|
||||||
governor = "0.10.4"
|
governor = "0.10.4"
|
||||||
|
strum = { version = "0.25", features = ["derive", "strum_macros"] }
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ use axum::{
|
|||||||
BoxError,
|
BoxError,
|
||||||
};
|
};
|
||||||
use rust_embed::RustEmbed;
|
use rust_embed::RustEmbed;
|
||||||
use serde::Deserialize;
|
|
||||||
use shared::{
|
use shared::{
|
||||||
GlobalLimitRequest, SetFilePriorityRequest, SetLabelRequest, TorrentActionRequest, TorrentFile,
|
AddTorrentRequest, GlobalLimitRequest, SetFilePriorityRequest, SetLabelRequest, TorrentActionRequest,
|
||||||
TorrentPeer, TorrentTracker,
|
TorrentFile, TorrentPeer, TorrentTracker,
|
||||||
};
|
};
|
||||||
use utoipa::ToSchema;
|
|
||||||
|
|
||||||
pub mod auth;
|
pub mod auth;
|
||||||
pub mod setup;
|
pub mod setup;
|
||||||
@@ -25,13 +23,6 @@ pub mod setup;
|
|||||||
#[folder = "../frontend/dist"]
|
#[folder = "../frontend/dist"]
|
||||||
pub struct Asset;
|
pub struct Asset;
|
||||||
|
|
||||||
#[derive(Deserialize, ToSchema)]
|
|
||||||
pub struct AddTorrentRequest {
|
|
||||||
/// Magnet link or Torrent file URL
|
|
||||||
#[schema(example = "magnet:?xt=urn:btih:...")]
|
|
||||||
uri: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub async fn static_handler(uri: Uri) -> impl IntoResponse {
|
pub async fn static_handler(uri: Uri) -> impl IntoResponse {
|
||||||
let mut path = uri.path().trim_start_matches('/').to_string();
|
let mut path = uri.path().trim_start_matches('/').to_string();
|
||||||
|
|
||||||
|
|||||||
@@ -125,7 +125,7 @@ struct Args {
|
|||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
handlers::AddTorrentRequest,
|
shared::AddTorrentRequest,
|
||||||
shared::TorrentActionRequest,
|
shared::TorrentActionRequest,
|
||||||
shared::Torrent,
|
shared::Torrent,
|
||||||
shared::TorrentStatus,
|
shared::TorrentStatus,
|
||||||
@@ -172,7 +172,7 @@ struct ApiDoc;
|
|||||||
),
|
),
|
||||||
components(
|
components(
|
||||||
schemas(
|
schemas(
|
||||||
handlers::AddTorrentRequest,
|
shared::AddTorrentRequest,
|
||||||
shared::TorrentActionRequest,
|
shared::TorrentActionRequest,
|
||||||
shared::Torrent,
|
shared::Torrent,
|
||||||
shared::TorrentStatus,
|
shared::TorrentStatus,
|
||||||
|
|||||||
@@ -7,95 +7,80 @@ use axum::response::sse::{Event, Sse};
|
|||||||
use futures::stream::{self, Stream};
|
use futures::stream::{self, Stream};
|
||||||
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
||||||
use std::convert::Infallible;
|
use std::convert::Infallible;
|
||||||
|
use strum::{Display, EnumString};
|
||||||
use tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
|
||||||
// Field definitions to keep query and parser in sync
|
#[derive(Debug, Clone, Copy, EnumString, Display, PartialEq)]
|
||||||
mod fields {
|
enum RtorrentField {
|
||||||
pub const IDX_HASH: usize = 0;
|
#[strum(serialize = "d.hash=")]
|
||||||
pub const CMD_HASH: &str = "d.hash=";
|
Hash,
|
||||||
|
#[strum(serialize = "d.name=")]
|
||||||
pub const IDX_NAME: usize = 1;
|
Name,
|
||||||
pub const CMD_NAME: &str = "d.name=";
|
#[strum(serialize = "d.size_bytes=")]
|
||||||
|
Size,
|
||||||
pub const IDX_SIZE: usize = 2;
|
#[strum(serialize = "d.bytes_done=")]
|
||||||
pub const CMD_SIZE: &str = "d.size_bytes=";
|
Completed,
|
||||||
|
#[strum(serialize = "d.down.rate=")]
|
||||||
pub const IDX_COMPLETED: usize = 3;
|
DownRate,
|
||||||
pub const CMD_COMPLETED: &str = "d.bytes_done=";
|
#[strum(serialize = "d.up.rate=")]
|
||||||
|
UpRate,
|
||||||
pub const IDX_DOWN_RATE: usize = 4;
|
#[strum(serialize = "d.state=")]
|
||||||
pub const CMD_DOWN_RATE: &str = "d.down.rate=";
|
State,
|
||||||
|
#[strum(serialize = "d.complete=")]
|
||||||
pub const IDX_UP_RATE: usize = 5;
|
Complete,
|
||||||
pub const CMD_UP_RATE: &str = "d.up.rate=";
|
#[strum(serialize = "d.message=")]
|
||||||
|
Message,
|
||||||
pub const IDX_STATE: usize = 6;
|
#[strum(serialize = "d.left_bytes=")]
|
||||||
pub const CMD_STATE: &str = "d.state=";
|
LeftBytes,
|
||||||
|
#[strum(serialize = "d.creation_date=")]
|
||||||
pub const IDX_COMPLETE: usize = 7;
|
CreationDate,
|
||||||
pub const CMD_COMPLETE: &str = "d.complete=";
|
#[strum(serialize = "d.hashing=")]
|
||||||
|
Hashing,
|
||||||
pub const IDX_MESSAGE: usize = 8;
|
#[strum(serialize = "d.custom1=")]
|
||||||
pub const CMD_MESSAGE: &str = "d.message=";
|
Label,
|
||||||
|
|
||||||
pub const IDX_LEFT_BYTES: usize = 9;
|
|
||||||
pub const CMD_LEFT_BYTES: &str = "d.left_bytes=";
|
|
||||||
|
|
||||||
pub const IDX_CREATION_DATE: usize = 10;
|
|
||||||
pub const CMD_CREATION_DATE: &str = "d.creation_date=";
|
|
||||||
|
|
||||||
pub const IDX_HASHING: usize = 11;
|
|
||||||
pub const CMD_HASHING: &str = "d.hashing=";
|
|
||||||
|
|
||||||
pub const IDX_LABEL: usize = 12;
|
|
||||||
pub const CMD_LABEL: &str = "d.custom1=";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
use fields::*;
|
const RTORRENT_FIELDS: &[RtorrentField] = &[
|
||||||
|
RtorrentField::Hash,
|
||||||
// Constants for rTorrent fields to ensure query and parser stay in sync
|
RtorrentField::Name,
|
||||||
const RTORRENT_FIELDS: &[&str] = &[
|
RtorrentField::Size,
|
||||||
"", // Ignored by multicall pattern
|
RtorrentField::Completed,
|
||||||
"main", // View
|
RtorrentField::DownRate,
|
||||||
CMD_HASH,
|
RtorrentField::UpRate,
|
||||||
CMD_NAME,
|
RtorrentField::State,
|
||||||
CMD_SIZE,
|
RtorrentField::Complete,
|
||||||
CMD_COMPLETED,
|
RtorrentField::Message,
|
||||||
CMD_DOWN_RATE,
|
RtorrentField::LeftBytes,
|
||||||
CMD_UP_RATE,
|
RtorrentField::CreationDate,
|
||||||
CMD_STATE,
|
RtorrentField::Hashing,
|
||||||
CMD_COMPLETE,
|
RtorrentField::Label,
|
||||||
CMD_MESSAGE,
|
|
||||||
CMD_LEFT_BYTES,
|
|
||||||
CMD_CREATION_DATE,
|
|
||||||
CMD_HASHING,
|
|
||||||
CMD_LABEL,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
fn parse_long(s: Option<&String>) -> i64 {
|
fn get_field_value(row: &Vec<String>, field: RtorrentField) -> String {
|
||||||
s.map(|v| v.parse().unwrap_or(0)).unwrap_or(0)
|
let idx = RTORRENT_FIELDS.iter().position(|&f| f == field).unwrap_or(0);
|
||||||
|
row.get(idx).cloned().unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_string(s: Option<&String>) -> String {
|
fn parse_long(s: &str) -> i64 {
|
||||||
s.cloned().unwrap_or_default()
|
s.parse().unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts a raw row of strings from rTorrent XML-RPC into a generic Torrent struct
|
/// Converts a raw row of strings from rTorrent XML-RPC into a generic Torrent struct
|
||||||
fn from_rtorrent_row(row: Vec<String>) -> Torrent {
|
fn from_rtorrent_row(row: &Vec<String>) -> Torrent {
|
||||||
let hash = parse_string(row.get(IDX_HASH));
|
let hash = get_field_value(row, RtorrentField::Hash);
|
||||||
let name = parse_string(row.get(IDX_NAME));
|
let name = get_field_value(row, RtorrentField::Name);
|
||||||
let size = parse_long(row.get(IDX_SIZE));
|
let size = parse_long(&get_field_value(row, RtorrentField::Size));
|
||||||
let completed = parse_long(row.get(IDX_COMPLETED));
|
let completed = parse_long(&get_field_value(row, RtorrentField::Completed));
|
||||||
let down_rate = parse_long(row.get(IDX_DOWN_RATE));
|
let down_rate = parse_long(&get_field_value(row, RtorrentField::DownRate));
|
||||||
let up_rate = parse_long(row.get(IDX_UP_RATE));
|
let up_rate = parse_long(&get_field_value(row, RtorrentField::UpRate));
|
||||||
|
|
||||||
let state = parse_long(row.get(IDX_STATE));
|
let state = parse_long(&get_field_value(row, RtorrentField::State));
|
||||||
let is_complete = parse_long(row.get(IDX_COMPLETE));
|
let is_complete = parse_long(&get_field_value(row, RtorrentField::Complete));
|
||||||
let message = parse_string(row.get(IDX_MESSAGE));
|
let message = get_field_value(row, RtorrentField::Message);
|
||||||
let left_bytes = parse_long(row.get(IDX_LEFT_BYTES));
|
let left_bytes = parse_long(&get_field_value(row, RtorrentField::LeftBytes));
|
||||||
let added_date = parse_long(row.get(IDX_CREATION_DATE));
|
let added_date = parse_long(&get_field_value(row, RtorrentField::CreationDate));
|
||||||
let is_hashing = parse_long(row.get(IDX_HASHING));
|
let is_hashing = parse_long(&get_field_value(row, RtorrentField::Hashing));
|
||||||
let label_raw = parse_string(row.get(IDX_LABEL));
|
let label_raw = get_field_value(row, RtorrentField::Label);
|
||||||
|
|
||||||
let label = if label_raw.is_empty() {
|
let label = if label_raw.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -146,7 +131,10 @@ fn from_rtorrent_row(row: Vec<String>) -> Torrent {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, XmlRpcError> {
|
pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, XmlRpcError> {
|
||||||
let params: Vec<RpcParam> = RTORRENT_FIELDS.iter().map(|s| RpcParam::from(*s)).collect();
|
let params: Vec<RpcParam> = RTORRENT_FIELDS
|
||||||
|
.iter()
|
||||||
|
.map(|&f| RpcParam::from(f.to_string()))
|
||||||
|
.collect();
|
||||||
let xml = client.call("d.multicall2", ¶ms).await?;
|
let xml = client.call("d.multicall2", ¶ms).await?;
|
||||||
|
|
||||||
if xml.trim().is_empty() {
|
if xml.trim().is_empty() {
|
||||||
@@ -155,7 +143,7 @@ pub async fn fetch_torrents(client: &RtorrentClient) -> Result<Vec<Torrent>, Xml
|
|||||||
|
|
||||||
let rows = parse_multicall_response(&xml)?;
|
let rows = parse_multicall_response(&xml)?;
|
||||||
|
|
||||||
let torrents = rows.into_iter().map(from_rtorrent_row).collect();
|
let torrents = rows.iter().map(from_rtorrent_row).collect();
|
||||||
|
|
||||||
Ok(torrents)
|
Ok(torrents)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="utf-8" />
|
<meta charset="utf-8" />
|
||||||
<meta
|
<meta
|
||||||
name="viewport"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
<title>VibeTorrent</title>
|
<title>VibeTorrent</title>
|
||||||
|
|
||||||
@@ -187,6 +187,17 @@
|
|||||||
|
|
||||||
<!-- Service Worker Registration & PWA Setup -->
|
<!-- Service Worker Registration & PWA Setup -->
|
||||||
<script>
|
<script>
|
||||||
|
// Global Dropdown Closer for iOS/Mobile
|
||||||
|
document.addEventListener('click', function(event) {
|
||||||
|
const details = document.querySelectorAll('details[open]');
|
||||||
|
details.forEach(detail => {
|
||||||
|
// Eğer tıklanan yer bu details'in içinde değilse kapat
|
||||||
|
if (!detail.contains(event.target)) {
|
||||||
|
detail.removeAttribute('open');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, true); // Use capture phase for better mobile support
|
||||||
|
|
||||||
if ("serviceWorker" in navigator) {
|
if ("serviceWorker" in navigator) {
|
||||||
window.addEventListener("load", () => {
|
window.addEventListener("load", () => {
|
||||||
navigator.serviceWorker
|
navigator.serviceWorker
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_use::on_click_outside;
|
|
||||||
use leptos_use::storage::use_local_storage;
|
use leptos_use::storage::use_local_storage;
|
||||||
use codee::string::FromToStringCodec;
|
use codee::string::FromToStringCodec;
|
||||||
use shared::GlobalLimitRequest;
|
use shared::GlobalLimitRequest;
|
||||||
@@ -97,7 +96,7 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Refs for click outside detection
|
// Refs for click outside detection (Handled globally via JS in index.html for better iOS support)
|
||||||
let down_details_ref = create_node_ref::<html::Details>();
|
let down_details_ref = create_node_ref::<html::Details>();
|
||||||
let up_details_ref = create_node_ref::<html::Details>();
|
let up_details_ref = create_node_ref::<html::Details>();
|
||||||
let theme_details_ref = create_node_ref::<html::Details>();
|
let theme_details_ref = create_node_ref::<html::Details>();
|
||||||
@@ -109,10 +108,6 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let _ = on_click_outside(down_details_ref, move |_| close_details(down_details_ref));
|
|
||||||
let _ = on_click_outside(up_details_ref, move |_| close_details(up_details_ref));
|
|
||||||
let _ = on_click_outside(theme_details_ref, move |_| close_details(theme_details_ref));
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="fixed bottom-0 left-0 right-0 h-8 min-h-8 bg-base-200 border-t border-base-300 flex items-center px-4 text-xs gap-4 text-base-content/70 z-[99] cursor-pointer">
|
<div class="fixed bottom-0 left-0 right-0 h-8 min-h-8 bg-base-200 border-t border-base-300 flex items-center px-4 text-xs gap-4 text-base-content/70 z-[99] cursor-pointer">
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos::html::Dialog;
|
use leptos::html::Dialog;
|
||||||
use crate::store::{show_toast_with_signal, TorrentStore};
|
use crate::store::{show_toast_with_signal, TorrentStore};
|
||||||
use shared::NotificationLevel;
|
use shared::{AddTorrentRequest, NotificationLevel};
|
||||||
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -36,9 +36,7 @@ pub fn AddTorrentModal(
|
|||||||
set_error_msg.set(None);
|
set_error_msg.set(None);
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let req_body = serde_json::json!({
|
let req_body = AddTorrentRequest { uri: uri_val };
|
||||||
"uri": uri_val
|
|
||||||
});
|
|
||||||
|
|
||||||
match gloo_net::http::Request::post("/api/torrents/add")
|
match gloo_net::http::Request::post("/api/torrents/add")
|
||||||
.json(&req_body)
|
.json(&req_body)
|
||||||
|
|||||||
@@ -131,6 +131,9 @@ pub fn provide_torrent_store() {
|
|||||||
let notifications = create_rw_signal(Vec::<NotificationItem>::new());
|
let notifications = create_rw_signal(Vec::<NotificationItem>::new());
|
||||||
let user = create_rw_signal(Option::<String>::None);
|
let user = create_rw_signal(Option::<String>::None);
|
||||||
|
|
||||||
|
// Browser notification hook
|
||||||
|
let show_browser_notification = crate::utils::notification::use_app_notification();
|
||||||
|
|
||||||
let store = TorrentStore {
|
let store = TorrentStore {
|
||||||
torrents,
|
torrents,
|
||||||
filter,
|
filter,
|
||||||
@@ -149,6 +152,7 @@ pub fn provide_torrent_store() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let show_browser_notification = show_browser_notification.clone();
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
let mut backoff_ms: u32 = 1000; // Start with 1 second
|
||||||
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
let max_backoff_ms: u32 = 30000; // Max 30 seconds
|
||||||
@@ -247,7 +251,7 @@ pub fn provide_torrent_store() {
|
|||||||
shared::NotificationLevel::Info => "ℹ️ VibeTorrent",
|
shared::NotificationLevel::Info => "ℹ️ VibeTorrent",
|
||||||
};
|
};
|
||||||
|
|
||||||
crate::utils::notification::show_notification_if_enabled(
|
show_browser_notification(
|
||||||
title,
|
title,
|
||||||
&n.message
|
&n.message
|
||||||
);
|
);
|
||||||
@@ -329,49 +333,21 @@ pub async fn subscribe_to_push_notifications() {
|
|||||||
// First, request notification permission if not already granted
|
// First, request notification permission if not already granted
|
||||||
let window = web_sys::window().expect("window should exist");
|
let window = web_sys::window().expect("window should exist");
|
||||||
|
|
||||||
// Notification.permission is a static property, but web_sys exposes it via the Notification class instance or we check it manually.
|
let permission = crate::utils::notification::get_notification_permission();
|
||||||
// Actually, Notification::permission() is a static method in web_sys.
|
if permission == "default" {
|
||||||
match web_sys::Notification::permission() {
|
|
||||||
web_sys::NotificationPermission::Granted => {
|
|
||||||
log::info!("Notification permission already granted");
|
|
||||||
}
|
|
||||||
web_sys::NotificationPermission::Denied => {
|
|
||||||
log::warn!("Notification permission was denied");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
web_sys::NotificationPermission::Default => {
|
|
||||||
log::info!("Requesting notification permission...");
|
log::info!("Requesting notification permission...");
|
||||||
let permission_promise = match web_sys::Notification::request_permission() {
|
if !crate::utils::notification::request_notification_permission().await {
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to request notification permission: {:?}", e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
match wasm_bindgen_futures::JsFuture::from(permission_promise).await {
|
|
||||||
Ok(val) => {
|
|
||||||
let permission = val.as_string().unwrap_or_default();
|
|
||||||
if permission != "granted" {
|
|
||||||
log::warn!("Notification permission denied by user");
|
log::warn!("Notification permission denied by user");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
log::info!("Notification permission granted by user");
|
} else if permission == "denied" {
|
||||||
}
|
log::warn!("Notification permission was denied");
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to await notification permission: {:?}", e);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log::warn!("Unknown notification permission status");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("Notification permission granted! Proceeding with push subscription...");
|
log::info!("Notification permission granted! Proceeding with push subscription...");
|
||||||
|
|
||||||
|
|
||||||
// Get VAPID public key from backend
|
// Get VAPID public key from backend
|
||||||
let public_key_response = match Request::get("/api/push/public-key").send().await {
|
let public_key_response = match Request::get("/api/push/public-key").send().await {
|
||||||
Ok(resp) => resp,
|
Ok(resp) => resp,
|
||||||
|
|||||||
@@ -1,37 +1,19 @@
|
|||||||
use wasm_bindgen::prelude::*;
|
use wasm_bindgen::prelude::*;
|
||||||
use web_sys::{Notification, NotificationOptions};
|
use web_sys::{Notification, NotificationOptions};
|
||||||
|
use leptos::*;
|
||||||
|
use leptos_use::{use_web_notification, UseWebNotificationReturn, NotificationPermission};
|
||||||
|
|
||||||
/// Request browser notification permission from user
|
/// Request browser notification permission from user
|
||||||
pub async fn request_notification_permission() -> bool {
|
pub async fn request_notification_permission() -> bool {
|
||||||
let window = web_sys::window().expect("no global window");
|
if let Ok(promise) = Notification::request_permission() {
|
||||||
|
if let Ok(result) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||||
// Check if Notification API is available
|
return result.as_string().unwrap_or_default() == "granted";
|
||||||
if js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false) {
|
|
||||||
let notification = js_sys::Reflect::get(&window, &JsValue::from_str("Notification"))
|
|
||||||
.expect("Notification should exist");
|
|
||||||
|
|
||||||
// Request permission
|
|
||||||
let promise = js_sys::Reflect::get(¬ification, &JsValue::from_str("requestPermission"))
|
|
||||||
.expect("requestPermission should exist");
|
|
||||||
|
|
||||||
if let Ok(function) = promise.dyn_into::<js_sys::Function>() {
|
|
||||||
if let Ok(promise) = function.call0(¬ification) {
|
|
||||||
if let Ok(promise) = promise.dyn_into::<js_sys::Promise>() {
|
|
||||||
let result = wasm_bindgen_futures::JsFuture::from(promise).await;
|
|
||||||
|
|
||||||
if let Ok(permission) = result {
|
|
||||||
let permission_str = permission.as_string().unwrap_or_default();
|
|
||||||
return permission_str == "granted";
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if browser notifications are supported and permitted
|
/// Check if browser notifications are supported
|
||||||
pub fn is_notification_supported() -> bool {
|
pub fn is_notification_supported() -> bool {
|
||||||
let window = web_sys::window().expect("no global window");
|
let window = web_sys::window().expect("no global window");
|
||||||
js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false)
|
js_sys::Reflect::has(&window, &JsValue::from_str("Notification")).unwrap_or(false)
|
||||||
@@ -39,67 +21,61 @@ pub fn is_notification_supported() -> bool {
|
|||||||
|
|
||||||
/// Get current notification permission status
|
/// Get current notification permission status
|
||||||
pub fn get_notification_permission() -> String {
|
pub fn get_notification_permission() -> String {
|
||||||
if !is_notification_supported() {
|
match Notification::permission() {
|
||||||
return "unsupported".to_string();
|
web_sys::NotificationPermission::Granted => "granted".to_string(),
|
||||||
|
web_sys::NotificationPermission::Denied => "denied".to_string(),
|
||||||
|
web_sys::NotificationPermission::Default => "default".to_string(),
|
||||||
|
_ => "default".to_string(),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Hook for using browser notifications within Leptos components or effects.
|
||||||
|
/// This uses leptos-use for reactive permission tracking.
|
||||||
|
pub fn use_app_notification() -> impl Fn(&str, &str) + Clone {
|
||||||
|
let UseWebNotificationReturn { permission, .. } = use_web_notification();
|
||||||
|
|
||||||
|
move |title: &str, body: &str| {
|
||||||
|
// Check user preference from localStorage
|
||||||
let window = web_sys::window().expect("no global window");
|
let window = web_sys::window().expect("no global window");
|
||||||
let notification = js_sys::Reflect::get(&window, &JsValue::from_str("Notification"))
|
let storage = window.local_storage().ok().flatten();
|
||||||
.expect("Notification should exist");
|
let enabled = storage
|
||||||
|
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||||
|
.unwrap_or_else(|| "true".to_string());
|
||||||
|
|
||||||
let permission = js_sys::Reflect::get(¬ification, &JsValue::from_str("permission"))
|
// Use the reactive permission signal from leptos-use
|
||||||
.unwrap_or(JsValue::from_str("default"));
|
if enabled == "true" && permission.get() == NotificationPermission::Granted {
|
||||||
|
show_browser_notification(title, body);
|
||||||
permission.as_string().unwrap_or("default".to_string())
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show a browser notification
|
/// Show a browser notification (non-reactive version)
|
||||||
/// Returns true if notification was shown successfully
|
pub fn show_browser_notification(title: &str, body: &str) -> bool {
|
||||||
pub fn show_browser_notification(title: &str, body: &str, icon: Option<&str>) -> bool {
|
if get_notification_permission() != "granted" {
|
||||||
// Check permission first
|
|
||||||
let permission = get_notification_permission();
|
|
||||||
if permission != "granted" {
|
|
||||||
log::warn!("Notification permission not granted: {}", permission);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create notification options
|
|
||||||
let opts = NotificationOptions::new();
|
let opts = NotificationOptions::new();
|
||||||
opts.set_body(body);
|
opts.set_body(body);
|
||||||
opts.set_icon(icon.unwrap_or("/icon-192.png"));
|
opts.set_icon("/icon-192.png");
|
||||||
opts.set_badge("/icon-192.png");
|
|
||||||
opts.set_tag("vibetorrent");
|
opts.set_tag("vibetorrent");
|
||||||
opts.set_require_interaction(false);
|
|
||||||
opts.set_silent(Some(false));
|
|
||||||
|
|
||||||
// Create and show notification
|
|
||||||
match Notification::new_with_options(title, &opts) {
|
match Notification::new_with_options(title, &opts) {
|
||||||
Ok(_notification) => {
|
Ok(_) => true,
|
||||||
true
|
Err(_) => false,
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to create notification: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show notification only if enabled in settings and permission granted
|
/// Legacy helper for showing notification if enabled in settings
|
||||||
pub fn show_notification_if_enabled(title: &str, body: &str) -> bool {
|
pub fn show_notification_if_enabled(title: &str, body: &str) -> bool {
|
||||||
// Check localStorage for user preference
|
|
||||||
let window = web_sys::window().expect("no global window");
|
let window = web_sys::window().expect("no global window");
|
||||||
let storage = window.local_storage().ok().flatten();
|
let storage = window.local_storage().ok().flatten();
|
||||||
|
|
||||||
if let Some(storage) = storage {
|
|
||||||
let enabled = storage
|
let enabled = storage
|
||||||
.get_item("vibetorrent_browser_notifications")
|
.and_then(|s| s.get_item("vibetorrent_browser_notifications").ok().flatten())
|
||||||
.ok()
|
.unwrap_or_else(|| "true".to_string());
|
||||||
.flatten()
|
|
||||||
.unwrap_or("true".to_string());
|
|
||||||
|
|
||||||
if enabled == "true" {
|
if enabled == "true" {
|
||||||
return show_browser_notification(title, body, None);
|
return show_browser_notification(title, body);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
|
|||||||
@@ -130,3 +130,9 @@ pub struct SetLabelRequest {
|
|||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub label: String,
|
pub label: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
pub struct AddTorrentRequest {
|
||||||
|
#[schema(example = "magnet:?xt=urn:btih:...")]
|
||||||
|
pub uri: String,
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user