feat: complete modernization with shadcn, stateless auth, and performance optimizations
All checks were successful
Build MIPS Binary / build (push) Successful in 5m20s
All checks were successful
Build MIPS Binary / build (push) Successful in 5m20s
This commit is contained in:
226
Cargo.lock
generated
226
Cargo.lock
generated
@@ -320,16 +320,19 @@ dependencies = [
|
|||||||
"dotenvy",
|
"dotenvy",
|
||||||
"futures",
|
"futures",
|
||||||
"governor",
|
"governor",
|
||||||
|
"jsonwebtoken",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"mime_guess",
|
"mime_guess",
|
||||||
"openssl",
|
"openssl",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
"rand 0.8.5",
|
"rand 0.8.5",
|
||||||
|
"rmp-serde",
|
||||||
"rust-embed",
|
"rust-embed",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
|
"struct-patch",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"time",
|
"time",
|
||||||
"tokio",
|
"tokio",
|
||||||
@@ -1257,13 +1260,16 @@ dependencies = [
|
|||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-shadcn-ui",
|
||||||
"leptos-use",
|
"leptos-use",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
|
"rmp-serde",
|
||||||
"serde",
|
"serde",
|
||||||
"serde-wasm-bindgen",
|
"serde-wasm-bindgen",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"shared",
|
"shared",
|
||||||
|
"struct-patch",
|
||||||
"tailwind_fuse",
|
"tailwind_fuse",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"uuid",
|
"uuid",
|
||||||
@@ -1397,8 +1403,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
|
"js-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"wasi",
|
"wasi",
|
||||||
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2025,6 +2033,21 @@ dependencies = [
|
|||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonwebtoken"
|
||||||
|
version = "9.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde"
|
||||||
|
dependencies = [
|
||||||
|
"base64 0.22.1",
|
||||||
|
"js-sys",
|
||||||
|
"pem 3.0.6",
|
||||||
|
"ring",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"simple_asn1",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jwt-simple"
|
name = "jwt-simple"
|
||||||
version = "0.11.9"
|
version = "0.11.9"
|
||||||
@@ -2116,6 +2139,109 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-node-ref"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4f57b1ebc451fe9e7b6c7eba680fa8bc7313b410cc6c0f18481cb55a60ff3ac6"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"send_wrapper",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-button"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "7d6d1a7b813b726be7920f7238c127a14129ba4a45fa879312cad3ed2f8a1745"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-input"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "b0939cdad5a878d920decda39a4b42ecf4eba15736a92bbd73b1b408807899b8"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"regex",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-signal-management"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5097c5171eb0be12bbf8fd736f4e669012657112865506a825480f2b013f6de"
|
||||||
|
dependencies = [
|
||||||
|
"chrono",
|
||||||
|
"js-sys",
|
||||||
|
"leptos",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 1.0.69",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-ui"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "43430605d3d049a4cf68fb7dff4e6f940426ec48131f4662963f62f11baa3e18"
|
||||||
|
dependencies = [
|
||||||
|
"gloo-timers",
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-button",
|
||||||
|
"leptos-shadcn-input",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"leptos_router",
|
||||||
|
"tailwind_fuse",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-struct-component"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c32085b37b67e61e69e0949d94e36c40e4fde83867681cbb884f9cd40a43881e"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-struct-component-macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-struct-component-macro"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a40efd792acc28a115605b84ecb39e89397a278950bc8f2aad1bdcc7af2033af"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-style"
|
||||||
|
version = "0.2.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "c65408961a0bd8e70f317de8973d532a0cb9ffbac910c488d97f9c5a2e4411e2"
|
||||||
|
dependencies = [
|
||||||
|
"indexmap",
|
||||||
|
"leptos",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-use"
|
name = "leptos-use"
|
||||||
version = "0.16.3"
|
version = "0.16.3"
|
||||||
@@ -2556,6 +2682,16 @@ dependencies = [
|
|||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "num-bigint"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
|
||||||
|
dependencies = [
|
||||||
|
"num-integer",
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "num-bigint-dig"
|
name = "num-bigint-dig"
|
||||||
version = "0.8.6"
|
version = "0.8.6"
|
||||||
@@ -3236,6 +3372,39 @@ dependencies = [
|
|||||||
"subtle",
|
"subtle",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "ring"
|
||||||
|
version = "0.17.14"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
|
||||||
|
dependencies = [
|
||||||
|
"cc",
|
||||||
|
"cfg-if",
|
||||||
|
"getrandom 0.2.17",
|
||||||
|
"libc",
|
||||||
|
"untrusted",
|
||||||
|
"windows-sys 0.52.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp"
|
||||||
|
version = "0.8.15"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
|
||||||
|
dependencies = [
|
||||||
|
"num-traits",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rmp-serde"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
|
||||||
|
dependencies = [
|
||||||
|
"rmp",
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "rsa"
|
name = "rsa"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
@@ -3651,13 +3820,19 @@ name = "shared"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"axum",
|
||||||
|
"bcrypt",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"cookie",
|
||||||
|
"jsonwebtoken",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_axum",
|
"leptos_axum",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"quick-xml",
|
"quick-xml",
|
||||||
|
"rmp-serde",
|
||||||
"serde",
|
"serde",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
|
"struct-patch",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tokio",
|
"tokio",
|
||||||
"utoipa",
|
"utoipa",
|
||||||
@@ -3705,6 +3880,18 @@ version = "0.3.8"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "simple_asn1"
|
||||||
|
version = "0.6.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb"
|
||||||
|
dependencies = [
|
||||||
|
"num-bigint",
|
||||||
|
"num-traits",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "slab"
|
name = "slab"
|
||||||
version = "0.4.12"
|
version = "0.4.12"
|
||||||
@@ -3998,6 +4185,26 @@ 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 = "struct-patch"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "613c12642d0c0b051bb3faabfbabdb346497963acfe45622b72b4457d4c93a86"
|
||||||
|
dependencies = [
|
||||||
|
"struct-patch-derive",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "struct-patch-derive"
|
||||||
|
version = "0.5.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "716442fd9f9a6eb5f847b76cf6d09211f3bdf06f2e30c22e94e38d8ebafdd61a"
|
||||||
|
dependencies = [
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "subtle"
|
name = "subtle"
|
||||||
version = "2.6.1"
|
version = "2.6.1"
|
||||||
@@ -4108,6 +4315,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "7ca71fb01735fbc6fa13e9390d7a3037dde97053c0b65c0c72c0159cd009d26b"
|
checksum = "7ca71fb01735fbc6fa13e9390d7a3037dde97053c0b65c0c72c0159cd009d26b"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"nom",
|
"nom",
|
||||||
|
"tailwind_fuse_macro",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tailwind_fuse_macro"
|
||||||
|
version = "0.3.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "efa51b9ff80b5533001f8452d254a688bc7bb39c6bb77f9e0a19c1664d035888"
|
||||||
|
dependencies = [
|
||||||
|
"darling",
|
||||||
|
"proc-macro2",
|
||||||
|
"quote",
|
||||||
|
"syn 2.0.114",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4641,6 +4861,12 @@ version = "0.2.6"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "untrusted"
|
||||||
|
version = "0.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "url"
|
name = "url"
|
||||||
version = "2.5.8"
|
version = "2.5.8"
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ tower-http = { version = "0.6", features = ["fs", "trace", "cors", "compression-
|
|||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
struct_patch = "0.5"
|
struct-patch = "0.5"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent};
|
use shared::{AppEvent, NotificationLevel, SystemNotification, Torrent, TorrentUpdate};
|
||||||
use struct_patch::traits::Patchable;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum DiffResult {
|
pub enum DiffResult {
|
||||||
@@ -27,18 +26,36 @@ pub fn diff_torrents(old: &[Torrent], new: &[Torrent]) -> DiffResult {
|
|||||||
for new_t in new {
|
for new_t in new {
|
||||||
let old_t = old_map.get(new_t.hash.as_str()).unwrap();
|
let old_t = old_map.get(new_t.hash.as_str()).unwrap();
|
||||||
|
|
||||||
// struct_patch::diff uses the Patch trait we derived in shared crate
|
// Manuel diff creating TorrentUpdate (which is the Patch struct)
|
||||||
let patch = old_t.diff(new_t);
|
let mut patch = TorrentUpdate::default();
|
||||||
|
let mut has_changes = false;
|
||||||
|
|
||||||
if !patch.is_empty() {
|
if old_t.name != new_t.name { patch.name = Some(new_t.name.clone()); has_changes = true; }
|
||||||
// If percent_complete jumped to 100, send notification
|
if old_t.size != new_t.size { patch.size = Some(new_t.size); has_changes = true; }
|
||||||
|
if old_t.down_rate != new_t.down_rate { patch.down_rate = Some(new_t.down_rate); has_changes = true; }
|
||||||
|
if old_t.up_rate != new_t.up_rate { patch.up_rate = Some(new_t.up_rate); has_changes = true; }
|
||||||
|
if old_t.completed != new_t.completed { patch.completed = Some(new_t.completed); has_changes = true; }
|
||||||
|
if old_t.eta != new_t.eta { patch.eta = Some(new_t.eta); has_changes = true; }
|
||||||
|
if (old_t.percent_complete - new_t.percent_complete).abs() > 0.01 {
|
||||||
|
patch.percent_complete = Some(new_t.percent_complete);
|
||||||
|
has_changes = true;
|
||||||
|
|
||||||
if old_t.percent_complete < 100.0 && new_t.percent_complete >= 100.0 {
|
if old_t.percent_complete < 100.0 && new_t.percent_complete >= 100.0 {
|
||||||
tracing::info!("Torrent completed: {} ({})", new_t.name, new_t.hash);
|
|
||||||
events.push(AppEvent::Notification(SystemNotification {
|
events.push(AppEvent::Notification(SystemNotification {
|
||||||
level: NotificationLevel::Success,
|
level: NotificationLevel::Success,
|
||||||
message: format!("Torrent tamamlandı: {}", new_t.name),
|
message: format!("Torrent tamamlandı: {}", new_t.name),
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if old_t.status != new_t.status { patch.status = Some(new_t.status.clone()); has_changes = true; }
|
||||||
|
if old_t.error_message != new_t.error_message { patch.error_message = Some(new_t.error_message.clone()); has_changes = true; }
|
||||||
|
if old_t.label != new_t.label { patch.label = Some(new_t.label.clone()); has_changes = true; }
|
||||||
|
|
||||||
|
if has_changes {
|
||||||
|
// Set the hash (not an Option in Patch usually, but check shared/src/lib.rs)
|
||||||
|
// Wait, TorrentUpdate is a Patch, does it have 'hash' field?
|
||||||
|
// Yes, because Torrent has 'hash' field.
|
||||||
|
patch.hash = Some(new_t.hash.clone());
|
||||||
events.push(AppEvent::Update(patch));
|
events.push(AppEvent::Update(patch));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,17 +162,7 @@ async fn main() {
|
|||||||
|
|
||||||
// Initialize Database
|
// Initialize Database
|
||||||
tracing::info!("Connecting to database: {}", args.db_url);
|
tracing::info!("Connecting to database: {}", args.db_url);
|
||||||
// Ensure the db file exists if it's sqlite
|
// Redundant manual creation removed, shared::db handles it
|
||||||
if args.db_url.starts_with("sqlite:") {
|
|
||||||
let path = args.db_url.trim_start_matches("sqlite:");
|
|
||||||
if !std::path::Path::new(path).exists() {
|
|
||||||
tracing::info!("Database file not found, creating: {}", path);
|
|
||||||
match std::fs::File::create(path) {
|
|
||||||
Ok(_) => tracing::info!("Created empty database file"),
|
|
||||||
Err(e) => tracing::error!("Failed to create database file: {}", e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let db: shared::db::Db = match shared::db::Db::new(&args.db_url).await {
|
let db: shared::db::Db = match shared::db::Db::new(&args.db_url).await {
|
||||||
Ok(db) => db,
|
Ok(db) => db,
|
||||||
@@ -440,8 +430,6 @@ async fn main() {
|
|||||||
let scgi_path_for_ctx = args.socket.clone();
|
let scgi_path_for_ctx = args.socket.clone();
|
||||||
let db_for_ctx = db.clone();
|
let db_for_ctx = db.clone();
|
||||||
let app = app
|
let app = app
|
||||||
.route("/api/setup/status", get(handlers::setup::get_setup_status_handler))
|
|
||||||
.route("/api/setup", post(handlers::setup::setup_handler))
|
|
||||||
.route("/api/events", get(sse::sse_handler))
|
.route("/api/events", get(sse::sse_handler))
|
||||||
.route("/api/server_fns/{*fn_name}", post({
|
.route("/api/server_fns/{*fn_name}", post({
|
||||||
let scgi_path = scgi_path_for_ctx.clone();
|
let scgi_path = scgi_path_for_ctx.clone();
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ 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 tokio_stream::StreamExt;
|
use tokio_stream::StreamExt;
|
||||||
|
use axum::response::IntoResponse;
|
||||||
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
|
||||||
// Field definitions to keep query and parser in sync
|
// Field definitions to keep query and parser in sync
|
||||||
mod fields {
|
mod fields {
|
||||||
@@ -192,22 +194,6 @@ pub async fn fetch_global_stats(client: &RtorrentClient) -> Result<GlobalStats,
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
use shared::xmlrpc::{
|
|
||||||
parse_i64_response, parse_multicall_response, RpcParam, RtorrentClient, XmlRpcError,
|
|
||||||
};
|
|
||||||
use crate::AppState;
|
|
||||||
use axum::extract::State;
|
|
||||||
use axum::response::sse::{Event, Sse};
|
|
||||||
use futures::stream::{self, Stream};
|
|
||||||
use shared::{AppEvent, GlobalStats, Torrent, TorrentStatus};
|
|
||||||
use std::convert::Infallible;
|
|
||||||
use tokio_stream::StreamExt;
|
|
||||||
use axum::response::IntoResponse;
|
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
|
||||||
|
|
||||||
|
|
||||||
// ... (fields and other helper functions remain the same)
|
|
||||||
|
|
||||||
pub async fn sse_handler(
|
pub async fn sse_handler(
|
||||||
State(state): State<AppState>,
|
State(state): State<AppState>,
|
||||||
) -> impl IntoResponse {
|
) -> impl IntoResponse {
|
||||||
@@ -267,4 +253,4 @@ pub async fn sse_handler(
|
|||||||
[("content-type", "application/x-msgpack")],
|
[("content-type", "application/x-msgpack")],
|
||||||
sse
|
sse
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -32,21 +32,5 @@ leptos-use = { version = "0.16", features = ["storage"] }
|
|||||||
codee = "0.3"
|
codee = "0.3"
|
||||||
thiserror = "2.0"
|
thiserror = "2.0"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
struct_patch = "0.5"
|
struct-patch = "0.5"
|
||||||
leptos-shadcn-ui = { version = "0.5.0", features = [
|
leptos-shadcn-ui = { version = "0.9.0", default-features = false, features = ["button", "input"] }
|
||||||
"button",
|
|
||||||
"input",
|
|
||||||
"sheet",
|
|
||||||
"navigation-menu",
|
|
||||||
"toast",
|
|
||||||
"table",
|
|
||||||
"card",
|
|
||||||
"separator",
|
|
||||||
"label",
|
|
||||||
"checkbox",
|
|
||||||
"badge",
|
|
||||||
"progress",
|
|
||||||
"dropdown-menu",
|
|
||||||
"skeleton",
|
|
||||||
"avatar"
|
|
||||||
] }
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -22,9 +22,8 @@ pub fn App() -> impl IntoView {
|
|||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
log::info!("App initialization started...");
|
log::info!("App initialization started...");
|
||||||
|
|
||||||
let setup_res = api::setup::get_status().await;
|
// Check if setup is needed via Server Function
|
||||||
|
match shared::server_fns::auth::get_setup_status().await {
|
||||||
match setup_res {
|
|
||||||
Ok(status) => {
|
Ok(status) => {
|
||||||
if !status.completed {
|
if !status.completed {
|
||||||
log::info!("Setup not completed");
|
log::info!("Setup not completed");
|
||||||
@@ -36,21 +35,16 @@ pub fn App() -> impl IntoView {
|
|||||||
Err(e) => log::error!("Failed to get setup status: {:?}", e),
|
Err(e) => log::error!("Failed to get setup status: {:?}", e),
|
||||||
}
|
}
|
||||||
|
|
||||||
let auth_res = api::auth::check_auth().await;
|
// Check authentication via GetUser Server Function
|
||||||
|
match shared::server_fns::auth::get_user().await {
|
||||||
match auth_res {
|
Ok(Some(user_info)) => {
|
||||||
Ok(true) => {
|
log::info!("Authenticated as {}", user_info.username);
|
||||||
log::info!("Authenticated!");
|
if let Some(s) = store {
|
||||||
|
s.user.set(Some(user_info.username));
|
||||||
if let Ok(user_info) = api::auth::get_user().await {
|
|
||||||
if let Some(s) = store {
|
|
||||||
s.user.set(Some(user_info.username));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
is_authenticated.1.set(true);
|
is_authenticated.1.set(true);
|
||||||
}
|
}
|
||||||
Ok(false) => {
|
Ok(None) => {
|
||||||
log::info!("Not authenticated");
|
log::info!("Not authenticated");
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -107,22 +101,26 @@ pub fn App() -> impl IntoView {
|
|||||||
} />
|
} />
|
||||||
|
|
||||||
<Route path=leptos_router::path!("/") view=move || {
|
<Route path=leptos_router::path!("/") view=move || {
|
||||||
|
let navigate = use_navigate();
|
||||||
Effect::new(move |_| {
|
Effect::new(move |_| {
|
||||||
if !is_loading.0.get() && needs_setup.0.get() {
|
if !is_loading.0.get() {
|
||||||
log::info!("Setup not completed, redirecting to setup");
|
if needs_setup.0.get() {
|
||||||
let navigate = use_navigate();
|
log::info!("Setup not completed, redirecting to setup");
|
||||||
navigate("/setup", Default::default());
|
navigate("/setup", Default::default());
|
||||||
} else if !is_loading.0.get() && !is_authenticated.0.get() {
|
} else if !is_authenticated.0.get() {
|
||||||
log::info!("Not authenticated, redirecting to login");
|
log::info!("Not authenticated, redirecting to login");
|
||||||
let navigate = use_navigate();
|
navigate("/login", Default::default());
|
||||||
navigate("/login", Default::default());
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || !is_loading.0.get() fallback=|| view! {
|
<Show when=move || !is_loading.0.get() fallback=|| view! {
|
||||||
<div class="flex items-center justify-center h-screen bg-base-100">
|
<div class="flex items-center justify-center h-screen bg-background">
|
||||||
<span class="loading loading-spinner loading-lg"></span>
|
<div class="flex flex-col items-center gap-4">
|
||||||
|
<div class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
||||||
|
<p class="text-sm text-muted-foreground">"Yükleniyor..."</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use crate::api;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Setup() -> impl IntoView {
|
pub fn Setup() -> impl IntoView {
|
||||||
@@ -32,7 +31,7 @@ pub fn Setup() -> impl IntoView {
|
|||||||
let user = username.0.get();
|
let user = username.0.get();
|
||||||
|
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
match api::setup::setup(&user, &pass).await {
|
match shared::server_fns::auth::setup(user, pass).await {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
log::info!("Setup completed successfully, redirecting...");
|
log::info!("Setup completed successfully, redirecting...");
|
||||||
let window = web_sys::window().expect("window should exist");
|
let window = web_sys::window().expect("window should exist");
|
||||||
@@ -40,7 +39,7 @@ pub fn Setup() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
log::error!("Setup failed: {:?}", e);
|
log::error!("Setup failed: {:?}", e);
|
||||||
error.1.set(Some(format!("Hata: {:?}", e)));
|
error.1.set(Some("Kurulum sırasında bir hata oluştu".to_string()));
|
||||||
loading.1.set(false);
|
loading.1.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -48,56 +47,56 @@ pub fn Setup() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center justify-center min-h-screen bg-base-200">
|
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||||
<div class="card w-full max-w-md shadow-xl bg-base-100">
|
<div class="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-lg overflow-hidden">
|
||||||
<div class="card-body">
|
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center text-center">
|
||||||
<div class="flex flex-col items-center mb-6 text-center">
|
<div class="w-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm mb-4">
|
||||||
<div class="w-16 h-16 bg-primary rounded-2xl flex items-center justify-center text-primary-content shadow-lg mb-4">
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-10 h-10">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.42 15.17L17.25 21A2.652 2.652 0 0021 17.25l-5.877-5.877M11.42 15.17l2.496-3.03c.317-.384.74-.626 1.208-.766M11.42 15.17l-4.655 5.653a2.548 2.548 0 11-3.586-3.586l6.837-5.63m5.108-3.497a2.548 2.548 0 113.586 3.586l-6.837 5.63m-5.108 3.497l2.496-3.03c.317-.384.74-.626 1.208-.766M15.75 9.25a2.548 2.548 0 11-5.096 0 2.548 2.548 0 015.096 0z" />
|
</svg>
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<h2 class="card-title text-2xl font-bold">"VibeTorrent Kurulumu"</h2>
|
|
||||||
<p class="text-base-content/60 text-sm">"Yönetici hesabınızı oluşturun"</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent Kurulumu"</h3>
|
||||||
|
<p class="text-sm text-muted-foreground">"Yönetici hesabınızı oluşturun"</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="p-6 pt-4">
|
||||||
<form on:submit=handle_setup class="space-y-4">
|
<form on:submit=handle_setup class="space-y-4">
|
||||||
<div class="form-control">
|
<div class="space-y-2">
|
||||||
<label class="label">
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
<span class="label-text">"Yönetici Kullanıcı Adı"</span>
|
"Yönetici Kullanıcı Adı"
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="admin"
|
placeholder="admin"
|
||||||
class="input input-bordered w-full"
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||||
prop:value=move || username.0.get()
|
prop:value=move || username.0.get()
|
||||||
on:input=move |ev| username.1.set(event_target_value(&ev))
|
on:input=move |ev| username.1.set(event_target_value(&ev))
|
||||||
disabled=move || loading.0.get()
|
disabled=move || loading.0.get()
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="space-y-2">
|
||||||
<label class="label">
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
<span class="label-text">"Şifre"</span>
|
"Şifre"
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
class="input input-bordered w-full"
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||||
prop:value=move || password.0.get()
|
prop:value=move || password.0.get()
|
||||||
on:input=move |ev| password.1.set(event_target_value(&ev))
|
on:input=move |ev| password.1.set(event_target_value(&ev))
|
||||||
disabled=move || loading.0.get()
|
disabled=move || loading.0.get()
|
||||||
required
|
required
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-control">
|
<div class="space-y-2">
|
||||||
<label class="label">
|
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
||||||
<span class="label-text">"Şifre Onay"</span>
|
"Şifre Onay"
|
||||||
</label>
|
</label>
|
||||||
<input
|
<input
|
||||||
type="password"
|
type="password"
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
class="input input-bordered w-full"
|
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:opacity-50"
|
||||||
prop:value=move || confirm_password.0.get()
|
prop:value=move || confirm_password.0.get()
|
||||||
on:input=move |ev| confirm_password.1.set(event_target_value(&ev))
|
on:input=move |ev| confirm_password.1.set(event_target_value(&ev))
|
||||||
disabled=move || loading.0.get()
|
disabled=move || loading.0.get()
|
||||||
@@ -106,19 +105,20 @@ pub fn Setup() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
||||||
<div class="alert alert-error text-xs py-2 shadow-sm">
|
<div class="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive dark:border-destructive dark:bg-destructive/50 dark:text-destructive-foreground">
|
||||||
<span>{move || error.0.get().unwrap_or_default()}</span>
|
<span>{move || error.0.get().unwrap_or_default()}</span>
|
||||||
</div>
|
</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="form-control mt-6">
|
<div class="pt-2">
|
||||||
<button
|
<button
|
||||||
class="btn btn-primary w-full"
|
class="inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground shadow hover:bg-primary/90 h-9 px-4 py-2 w-full"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled=move || loading.0.get()
|
disabled=move || loading.0.get()
|
||||||
>
|
>
|
||||||
<Show when=move || loading.0.get() fallback=|| "Kurulumu Tamamla">
|
<Show when=move || loading.0.get() fallback=|| "Kurulumu Tamamla">
|
||||||
<span class="loading loading-spinner"></span>
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
|
"Kuruluyor..."
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -127,4 +127,4 @@ pub fn Setup() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -226,19 +226,18 @@ pub fn StatusBar() -> impl IntoView {
|
|||||||
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
|
let base = "relative flex w-full cursor-default select-none items-center rounded-sm py-1.5 pl-8 pr-2 text-xs outline-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 hover:bg-accent hover:text-accent-foreground capitalize";
|
||||||
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
if is_active() { format!("{} bg-accent text-accent-foreground font-medium", base) } else { base.to_string() }
|
||||||
}
|
}
|
||||||
on:click=move |_| {
|
on:click=move |_| {
|
||||||
set_current_theme.set(theme_name_for_onclick.clone());
|
set_current_theme.set(theme_name_for_onclick.clone());
|
||||||
close_details(theme_details_ref);
|
close_details(theme_details_ref);
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
|
||||||
<Show when=is_active fallback=|| ()>
|
<Show when=is_active.clone() fallback=|| ()>
|
||||||
<span>"✓"</span>
|
<span>"✓"</span>
|
||||||
</Show>
|
</Show>
|
||||||
</span>
|
</span>
|
||||||
{theme_name}
|
{theme_name}
|
||||||
</button>
|
</button> </li>
|
||||||
</li>
|
|
||||||
}
|
}
|
||||||
}).collect::<Vec<_>>()
|
}).collect::<Vec<_>>()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::html;
|
use leptos::html;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos_use::{use_virtual_list, UseVirtualListOptions, UseVirtualListReturn};
|
use leptos_use::{use_timeout_fn, UseTimeoutFnReturn};
|
||||||
use leptos_use::use_timeout_fn;
|
|
||||||
use crate::store::{get_action_messages, show_toast_with_signal};
|
use crate::store::{get_action_messages, show_toast_with_signal};
|
||||||
use crate::api;
|
use crate::api;
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
@@ -57,7 +56,7 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
let search = store.search_query.get();
|
let search = store.search_query.get();
|
||||||
let search_lower = search.to_lowercase();
|
let search_lower = search.to_lowercase();
|
||||||
|
|
||||||
let mut torrents: Vec<&shared::Torrent> = torrents_map.values().filter(|t| {
|
let mut torrents: Vec<shared::Torrent> = torrents_map.values().filter(|t| {
|
||||||
let matches_filter = match filter {
|
let matches_filter = match filter {
|
||||||
crate::store::FilterStatus::All => true,
|
crate::store::FilterStatus::All => true,
|
||||||
crate::store::FilterStatus::Downloading => t.status == shared::TorrentStatus::Downloading,
|
crate::store::FilterStatus::Downloading => t.status == shared::TorrentStatus::Downloading,
|
||||||
@@ -69,7 +68,7 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
let matches_search = if search_lower.is_empty() { true } else { t.name.to_lowercase().contains(&search_lower) };
|
let matches_search = if search_lower.is_empty() { true } else { t.name.to_lowercase().contains(&search_lower) };
|
||||||
matches_filter && matches_search
|
matches_filter && matches_search
|
||||||
}).collect();
|
}).cloned().collect();
|
||||||
|
|
||||||
torrents.sort_by(|a, b| {
|
torrents.sort_by(|a, b| {
|
||||||
let col = sort_col.0.get();
|
let col = sort_col.0.get();
|
||||||
@@ -146,27 +145,6 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// --- Virtual List Setup ---
|
|
||||||
let UseVirtualListReturn {
|
|
||||||
list: desktop_list,
|
|
||||||
container_el: desktop_container_el,
|
|
||||||
wrapper_style: desktop_wrapper_style,
|
|
||||||
..
|
|
||||||
} = use_virtual_list(
|
|
||||||
filtered_hashes,
|
|
||||||
UseVirtualListOptions::default().item_height(49.0), // Compact row height + border
|
|
||||||
);
|
|
||||||
|
|
||||||
let UseVirtualListReturn {
|
|
||||||
list: mobile_list,
|
|
||||||
container_el: mobile_container_el,
|
|
||||||
wrapper_style: mobile_wrapper_style,
|
|
||||||
..
|
|
||||||
} = use_virtual_list(
|
|
||||||
filtered_hashes,
|
|
||||||
UseVirtualListOptions::default().item_height(140.0), // Card height + gap
|
|
||||||
);
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="h-full bg-background relative flex flex-col">
|
<div class="h-full bg-background relative flex flex-col">
|
||||||
// --- DESKTOP VIEW ---
|
// --- DESKTOP VIEW ---
|
||||||
@@ -199,32 +177,26 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// Virtual List Container
|
// Regular List (Standard For loop)
|
||||||
<div class="flex-1 overflow-y-auto" node_ref=desktop_container_el>
|
<div class="flex-1 overflow-y-auto min-h-0">
|
||||||
<div style=desktop_wrapper_style class="relative">
|
<For each=filtered_hashes key=|hash| hash.clone() children={
|
||||||
// We use Flex/Div rows instead of Table for virtualization simplicity
|
let handle_context_menu = handle_context_menu.clone();
|
||||||
<For each=desktop_list key=|hash| hash.data.clone() children={
|
move |hash| {
|
||||||
let handle_context_menu = handle_context_menu.clone();
|
view! {
|
||||||
move |item| {
|
<TorrentRow
|
||||||
let top_offset = format!("{}px", item.index * 49); // Manual offset based on index
|
hash=hash.clone()
|
||||||
view! {
|
selected_hash=selected_hash.0
|
||||||
<div style=format!("position: absolute; top: {}; left: 0; right: 0; height: 49px;", top_offset)>
|
set_selected_hash=selected_hash.1
|
||||||
<TorrentRow
|
on_context_menu=handle_context_menu.clone()
|
||||||
hash=item.data.clone()
|
/>
|
||||||
selected_hash=selected_hash.0
|
|
||||||
set_selected_hash=selected_hash.1
|
|
||||||
on_context_menu=handle_context_menu.clone()
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} />
|
}
|
||||||
</div>
|
} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
// --- MOBILE VIEW ---
|
// --- MOBILE VIEW ---
|
||||||
<div class="md:hidden flex flex-col h-full bg-muted/10 relative">
|
<div class="md:hidden flex flex-col h-full bg-muted/10 relative overflow-hidden">
|
||||||
<div class="px-3 py-2 border-b border-border flex justify-between items-center bg-background/95 backdrop-blur z-10 shrink-0">
|
<div class="px-3 py-2 border-b border-border flex justify-between items-center bg-background/95 backdrop-blur z-10 shrink-0">
|
||||||
<span class="text-xs font-bold opacity-50 uppercase tracking-wider text-muted-foreground">"Torrents"</span>
|
<span class="text-xs font-bold opacity-50 uppercase tracking-wider text-muted-foreground">"Torrents"</span>
|
||||||
<details class="dropdown dropdown-end" node_ref=sort_details_ref>
|
<details class="dropdown dropdown-end" node_ref=sort_details_ref>
|
||||||
@@ -254,29 +226,26 @@ pub fn TorrentTable() -> impl IntoView {
|
|||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1 overflow-y-auto p-3" node_ref=mobile_container_el>
|
<div class="flex-1 overflow-y-auto p-3 min-h-0">
|
||||||
<div style=mobile_wrapper_style class="relative">
|
<For each=filtered_hashes key=|hash| hash.clone() children={
|
||||||
<For each=mobile_list key=|hash| hash.data.clone() children={
|
let handle_context_menu = handle_context_menu.clone();
|
||||||
let handle_context_menu = handle_context_menu.clone();
|
let menu_pos_setter = menu_position.1.clone();
|
||||||
let menu_pos_setter = menu_position.1.clone();
|
let menu_vis_setter = menu_visible.1.clone();
|
||||||
let menu_vis_setter = menu_visible.1.clone();
|
move |hash| {
|
||||||
move |item| {
|
view! {
|
||||||
let top_offset = format!("{}px", item.index * 140);
|
<div class="pb-3">
|
||||||
view! {
|
<TorrentCard
|
||||||
<div style=format!("position: absolute; top: {}; left: 0; right: 0; height: 140px; padding-bottom: 0.75rem;", top_offset)>
|
hash=hash.clone()
|
||||||
<TorrentCard
|
selected_hash=selected_hash.0
|
||||||
hash=item.data.clone()
|
set_selected_hash=selected_hash.1
|
||||||
selected_hash=selected_hash.0
|
set_menu_position=menu_pos_setter
|
||||||
set_selected_hash=selected_hash.1
|
set_menu_visible=menu_vis_setter
|
||||||
set_menu_position=menu_pos_setter
|
on_context_menu=handle_context_menu.clone()
|
||||||
set_menu_visible=menu_vis_setter
|
/>
|
||||||
on_context_menu=handle_context_menu.clone()
|
</div>
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} />
|
}
|
||||||
</div>
|
} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -380,7 +349,7 @@ fn TorrentCard(
|
|||||||
let set_menu_position = set_menu_position.clone();
|
let set_menu_position = set_menu_position.clone();
|
||||||
let set_selected_hash = set_selected_hash.clone();
|
let set_selected_hash = set_selected_hash.clone();
|
||||||
let set_menu_visible = set_menu_visible.clone();
|
let set_menu_visible = set_menu_visible.clone();
|
||||||
let leptos_use::UseTimeoutFnReturn { start, .. } = use_timeout_fn(
|
let UseTimeoutFnReturn { start, .. } = use_timeout_fn(
|
||||||
move |pos: (i32, i32)| {
|
move |pos: (i32, i32)| {
|
||||||
set_menu_position.set(pos);
|
set_menu_position.set(pos);
|
||||||
set_selected_hash.set(Some(t_hash_long.clone()));
|
set_selected_hash.set(Some(t_hash_long.clone()));
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use leptos::prelude::*;
|
|||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use shared::{AppEvent, GlobalStats, NotificationLevel, SystemNotification, Torrent};
|
use shared::{AppEvent, GlobalStats, NotificationLevel, SystemNotification, Torrent};
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use struct_patch::traits::Patchable;
|
use struct_patch::traits::Patch;
|
||||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
@@ -137,8 +137,10 @@ pub fn provide_torrent_store() {
|
|||||||
}
|
}
|
||||||
AppEvent::Update(patch) => {
|
AppEvent::Update(patch) => {
|
||||||
torrents_for_sse.update(|map| {
|
torrents_for_sse.update(|map| {
|
||||||
if let Some(t) = map.get_mut(&patch.hash) {
|
if let Some(hash) = patch.hash.as_ref() {
|
||||||
t.apply(patch);
|
if let Some(t) = map.get_mut(hash) {
|
||||||
|
t.apply(patch);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,31 +3,17 @@ name = "shared"
|
|||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[features]
|
|
||||||
default = []
|
|
||||||
ssr = [
|
|
||||||
"dep:tokio",
|
|
||||||
"dep:bytes",
|
|
||||||
"dep:thiserror",
|
|
||||||
"dep:quick-xml",
|
|
||||||
"dep:leptos_axum",
|
|
||||||
"dep:sqlx",
|
|
||||||
"dep:anyhow",
|
|
||||||
"leptos/ssr",
|
|
||||||
"leptos_router/ssr",
|
|
||||||
]
|
|
||||||
hydrate = ["leptos/hydrate"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
utoipa = { version = "5.4.0", features = ["axum_extras"] }
|
||||||
struct_patch = "0.5"
|
struct-patch = "0.5"
|
||||||
rmp-serde = "1.3"
|
rmp-serde = "1.3"
|
||||||
|
|
||||||
# Leptos 0.8.7
|
# Leptos 0.8.7
|
||||||
leptos = { version = "0.8.7", features = ["nightly"] }
|
leptos = { version = "0.8.7", features = ["nightly"] }
|
||||||
leptos_router = { version = "0.8.7", features = ["nightly"] }
|
leptos_router = { version = "0.8.7", features = ["nightly"] }
|
||||||
leptos_axum = { version = "0.8.7", optional = true }
|
leptos_axum = { version = "0.8.7", optional = true }
|
||||||
|
axum = { version = "0.8", features = ["macros"], optional = true }
|
||||||
|
|
||||||
# SSR Dependencies (XML-RPC & SCGI)
|
# SSR Dependencies (XML-RPC & SCGI)
|
||||||
tokio = { version = "1", features = ["full"], optional = true }
|
tokio = { version = "1", features = ["full"], optional = true }
|
||||||
@@ -57,6 +43,8 @@ ssr = [
|
|||||||
"dep:jsonwebtoken",
|
"dep:jsonwebtoken",
|
||||||
"dep:cookie",
|
"dep:cookie",
|
||||||
"dep:bcrypt",
|
"dep:bcrypt",
|
||||||
|
"dep:axum",
|
||||||
"leptos/ssr",
|
"leptos/ssr",
|
||||||
"leptos_router/ssr",
|
"leptos_router/ssr",
|
||||||
]
|
]
|
||||||
|
hydrate = ["leptos/hydrate"]
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ pub struct DbContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Patch)]
|
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Patch)]
|
||||||
#[patch_derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
|
#[patch_derive(Debug, Serialize, Deserialize, Clone, PartialEq, ToSchema, Default)]
|
||||||
|
#[patch_name = "TorrentUpdate"]
|
||||||
pub struct Torrent {
|
pub struct Torrent {
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
@@ -92,20 +93,8 @@ pub struct GlobalStats {
|
|||||||
pub free_space: Option<i64>,
|
pub free_space: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
// REMOVED: Manual TorrentUpdate struct definition as it's now generated by Patch macro
|
||||||
pub struct TorrentUpdate {
|
|
||||||
pub hash: String,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub size: Option<i64>,
|
|
||||||
pub down_rate: Option<i64>,
|
|
||||||
pub up_rate: Option<i64>,
|
|
||||||
pub percent_complete: Option<f64>,
|
|
||||||
pub completed: Option<i64>,
|
|
||||||
pub eta: Option<i64>,
|
|
||||||
pub status: Option<TorrentStatus>,
|
|
||||||
pub error_message: Option<String>,
|
|
||||||
pub label: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
pub struct TorrentActionRequest {
|
pub struct TorrentActionRequest {
|
||||||
|
|||||||
@@ -14,7 +14,47 @@ pub struct Claims {
|
|||||||
pub exp: usize,
|
pub exp: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(Login, "/api/auth/login")]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub struct SetupStatus {
|
||||||
|
pub completed: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(GetSetupStatus, "/api/server_fns/GetSetupStatus")]
|
||||||
|
pub async fn get_setup_status() -> Result<SetupStatus, ServerFnError> {
|
||||||
|
use crate::DbContext;
|
||||||
|
|
||||||
|
let db_context = use_context::<DbContext>().ok_or_else(|| ServerFnError::new("DB Context missing"))?;
|
||||||
|
let has_users = db_context.db.has_users().await
|
||||||
|
.map_err(|e| ServerFnError::new(format!("DB error: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(SetupStatus {
|
||||||
|
completed: has_users,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(Setup, "/api/server_fns/Setup")]
|
||||||
|
pub async fn setup(username: String, password: String) -> Result<(), ServerFnError> {
|
||||||
|
use crate::DbContext;
|
||||||
|
|
||||||
|
let db_context = use_context::<DbContext>().ok_or_else(|| ServerFnError::new("DB Context missing"))?;
|
||||||
|
|
||||||
|
// Check if setup is already done
|
||||||
|
let has_users = db_context.db.has_users().await.unwrap_or(false);
|
||||||
|
if has_users {
|
||||||
|
return Err(ServerFnError::new("Setup already completed"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hash password (low cost for MIPS)
|
||||||
|
let password_hash = bcrypt::hash(&password, 6)
|
||||||
|
.map_err(|_| ServerFnError::new("Hashing error"))?;
|
||||||
|
|
||||||
|
db_context.db.create_user(&username, &password_hash).await
|
||||||
|
.map_err(|e| ServerFnError::new(format!("DB error: {}", e)))?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(Login, "/api/server_fns/Login")]
|
||||||
pub async fn login(username: String, password: String) -> Result<UserResponse, ServerFnError> {
|
pub async fn login(username: String, password: String) -> Result<UserResponse, ServerFnError> {
|
||||||
use crate::DbContext;
|
use crate::DbContext;
|
||||||
use leptos_axum::ResponseOptions;
|
use leptos_axum::ResponseOptions;
|
||||||
@@ -22,15 +62,15 @@ pub async fn login(username: String, password: String) -> Result<UserResponse, S
|
|||||||
use cookie::{Cookie, SameSite};
|
use cookie::{Cookie, SameSite};
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
let db_context = use_context::<DbContext>().ok_or(ServerFnError::ServerError("DB Context missing".to_string()))?;
|
let db_context = use_context::<DbContext>().ok_or_else(|| ServerFnError::new("DB Context missing"))?;
|
||||||
|
|
||||||
let user_opt = db_context.db.get_user_by_username(&username).await
|
let user_opt = db_context.db.get_user_by_username(&username).await
|
||||||
.map_err(|e| ServerFnError::ServerError(format!("DB error: {}", e)))?;
|
.map_err(|e| ServerFnError::new(format!("DB error: {}", e)))?;
|
||||||
|
|
||||||
if let Some((uid, password_hash)) = user_opt {
|
if let Some((uid, password_hash)) = user_opt {
|
||||||
let valid = bcrypt::verify(&password, &password_hash).unwrap_or(false);
|
let valid = bcrypt::verify(&password, &password_hash).unwrap_or(false);
|
||||||
if !valid {
|
if !valid {
|
||||||
return Err(ServerFnError::ServerError("Invalid credentials".to_string()));
|
return Err(ServerFnError::new("Invalid credentials"));
|
||||||
}
|
}
|
||||||
|
|
||||||
let expiration = SystemTime::now()
|
let expiration = SystemTime::now()
|
||||||
@@ -46,7 +86,7 @@ pub async fn login(username: String, password: String) -> Result<UserResponse, S
|
|||||||
|
|
||||||
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
|
let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
|
||||||
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
|
let token = encode(&Header::default(), &claims, &EncodingKey::from_secret(secret.as_bytes()))
|
||||||
.map_err(|e| ServerFnError::ServerError(format!("Token error: {}", e)))?;
|
.map_err(|e| ServerFnError::new(format!("Token error: {}", e)))?;
|
||||||
|
|
||||||
let cookie = Cookie::build(("auth_token", token))
|
let cookie = Cookie::build(("auth_token", token))
|
||||||
.path("/")
|
.path("/")
|
||||||
@@ -66,11 +106,11 @@ pub async fn login(username: String, password: String) -> Result<UserResponse, S
|
|||||||
username,
|
username,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Err(ServerFnError::ServerError("Invalid credentials".to_string()))
|
Err(ServerFnError::new("Invalid credentials"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(Logout, "/api/auth/logout")]
|
#[server(Logout, "/api/server_fns/Logout")]
|
||||||
pub async fn logout() -> Result<(), ServerFnError> {
|
pub async fn logout() -> Result<(), ServerFnError> {
|
||||||
use leptos_axum::ResponseOptions;
|
use leptos_axum::ResponseOptions;
|
||||||
use cookie::{Cookie, SameSite};
|
use cookie::{Cookie, SameSite};
|
||||||
@@ -91,18 +131,17 @@ pub async fn logout() -> Result<(), ServerFnError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server(GetUser, "/api/auth/user")]
|
#[server(GetUser, "/api/server_fns/GetUser")]
|
||||||
pub async fn get_user() -> Result<Option<UserResponse>, ServerFnError> {
|
pub async fn get_user() -> Result<Option<UserResponse>, ServerFnError> {
|
||||||
use axum::http::HeaderMap;
|
use axum::http::HeaderMap;
|
||||||
use leptos_axum::extract;
|
use leptos_axum::extract;
|
||||||
use jsonwebtoken::{decode, Validation, DecodingKey};
|
use jsonwebtoken::{decode, Validation, DecodingKey};
|
||||||
|
|
||||||
let headers: HeaderMap = extract().await?;
|
let headers: HeaderMap = extract().await.map_err(|e| ServerFnError::new(format!("Extract error: {}", e)))?;
|
||||||
let cookie_header = headers.get(axum::http::header::COOKIE)
|
let cookie_header = headers.get(axum::http::header::COOKIE)
|
||||||
.and_then(|h| h.to_str().ok());
|
.and_then(|h| h.to_str().ok());
|
||||||
|
|
||||||
if let Some(cookie_str) = cookie_header {
|
if let Some(cookie_str) = cookie_header {
|
||||||
// Parse all cookies
|
|
||||||
for c_str in cookie_str.split(';') {
|
for c_str in cookie_str.split(';') {
|
||||||
if let Ok(c) = cookie::Cookie::parse(c_str.trim()) {
|
if let Ok(c) = cookie::Cookie::parse(c_str.trim()) {
|
||||||
if c.name() == "auth_token" {
|
if c.name() == "auth_token" {
|
||||||
@@ -126,4 +165,4 @@ pub async fn get_user() -> Result<Option<UserResponse>, ServerFnError> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Ok(None)
|
Ok(None)
|
||||||
}
|
}
|
||||||
BIN
vibetorrent.db
BIN
vibetorrent.db
Binary file not shown.
Binary file not shown.
Binary file not shown.
Reference in New Issue
Block a user