feat(frontend): rewrite frontend with minimal Transmission-like design using DaisyUI
This commit is contained in:
155
Cargo.lock
generated
155
Cargo.lock
generated
@@ -97,15 +97,6 @@ version = "1.0.100"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
|
||||
|
||||
[[package]]
|
||||
name = "approx"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cab112f0a86d568ea0e627cc1d6be74a1e9cd55214684db5561995f6dad897c6"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.37"
|
||||
@@ -323,12 +314,6 @@ version = "3.19.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510"
|
||||
|
||||
[[package]]
|
||||
name = "by_address"
|
||||
version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "64fa3c856b712db6612c019f14756e64e4bcea13337a6b33b696333a9eaa2d06"
|
||||
|
||||
[[package]]
|
||||
name = "byteorder"
|
||||
version = "1.5.0"
|
||||
@@ -659,12 +644,6 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fast-srgb8"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd2e7510819d6fbf51a5545c8f922716ecfb14df168a3242f7d33e0239efe6a1"
|
||||
|
||||
[[package]]
|
||||
name = "find-msvc-tools"
|
||||
version = "0.1.8"
|
||||
@@ -711,7 +690,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shared",
|
||||
"tailwind_fuse",
|
||||
"thaw",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
@@ -1057,21 +1035,6 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_ai"
|
||||
version = "0.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9bf3a9c196a6a169f790639ecc8fdd4396660b1d53b905230bf0b364776a56fc"
|
||||
dependencies = [
|
||||
"icondata_core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icondata_core"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c97be924215abd5e630d84e95a47c710138a6559b4c55039f4f33aa897fa859"
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.1.1"
|
||||
@@ -1563,30 +1526,6 @@ version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "56d80efc4b6721e8be2a10a5df21a30fa0b470f1539e53d8b4e6e75faf938b63"
|
||||
|
||||
[[package]]
|
||||
name = "palette"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4cbf71184cc5ecc2e4e1baccdb21026c20e5fc3dcf63028a086131b3ab00b6e6"
|
||||
dependencies = [
|
||||
"approx",
|
||||
"fast-srgb8",
|
||||
"palette_derive",
|
||||
"phf",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "palette_derive"
|
||||
version = "0.7.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f5030daf005bface118c096f510ffb781fc28f9ab6a32ab224d8631be6851d30"
|
||||
dependencies = [
|
||||
"by_address",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.12.5"
|
||||
@@ -1628,48 +1567,6 @@ version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
||||
|
||||
[[package]]
|
||||
name = "phf"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
|
||||
dependencies = [
|
||||
"phf_macros",
|
||||
"phf_shared",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_generator"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
|
||||
dependencies = [
|
||||
"phf_shared",
|
||||
"rand",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_macros"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
|
||||
dependencies = [
|
||||
"phf_generator",
|
||||
"phf_shared",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "phf_shared"
|
||||
version = "0.11.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
|
||||
dependencies = [
|
||||
"siphasher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pin-project"
|
||||
version = "1.1.10"
|
||||
@@ -2245,12 +2142,6 @@ version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
|
||||
|
||||
[[package]]
|
||||
name = "siphasher"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.11"
|
||||
@@ -2344,52 +2235,6 @@ dependencies = [
|
||||
"nom",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thaw"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a2ea14de27ddd0ce167c69e78852122c5a9c755b3c083c8249e491c4a8821ce"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"icondata_ai",
|
||||
"icondata_core",
|
||||
"leptos",
|
||||
"num-traits",
|
||||
"palette",
|
||||
"thaw_components",
|
||||
"thaw_utils",
|
||||
"uuid",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thaw_components"
|
||||
version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f4ee9d9598f7a5162845a83ef0ed3ccde52c23d987cf64433056b2fd4542161c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"leptos",
|
||||
"thaw_utils",
|
||||
"uuid",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thaw_utils"
|
||||
version = "0.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8d5fe5c99ba6b6730ec533810dd4d92422b778d4fbaffa9b6d7fb705b28b3978"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"chrono",
|
||||
"leptos",
|
||||
"wasm-bindgen",
|
||||
"web-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.69"
|
||||
|
||||
@@ -21,4 +21,3 @@ chrono = { version = "0.4", features = ["serde"] }
|
||||
web-sys = { version = "0.3", features = ["Window", "Storage"] }
|
||||
shared = { path = "../shared" }
|
||||
tailwind_fuse = "0.3.2"
|
||||
thaw = "0.3"
|
||||
|
||||
@@ -20,21 +20,12 @@
|
||||
<link data-trunk rel="copy-file" href="icon-192.png" />
|
||||
<script>
|
||||
(function () {
|
||||
var t = localStorage.getItem("vibetorrent_theme");
|
||||
var c = "#0f172a"; // Midnight (default)
|
||||
var tc = "#94a3b8"; // Text Color (Slate 400)
|
||||
var t = localStorage.getItem("vibetorrent_theme") || "dark";
|
||||
if (t === "Amoled") t = "black";
|
||||
if (t === "Light") t = "light";
|
||||
if (t === "Dark" || t === "Midnight") t = "dark";
|
||||
|
||||
if (t === "Light") {
|
||||
c = "#f9fafb"; // Gray 50
|
||||
tc = "#111827"; // Gray 900
|
||||
} else if (t === "Amoled") {
|
||||
c = "#000000";
|
||||
tc = "#e5e7eb"; // Gray 200
|
||||
}
|
||||
|
||||
var s = document.createElement("style");
|
||||
s.innerHTML = "body { background-color: " + c + "; color: " + tc + "; }";
|
||||
document.head.appendChild(s);
|
||||
document.documentElement.setAttribute("data-theme", t.toLowerCase());
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
@@ -1,165 +1,11 @@
|
||||
@import "tailwindcss";
|
||||
@config "./tailwind.config.js";
|
||||
@plugin "daisyui";
|
||||
|
||||
@layer base {
|
||||
|
||||
html,
|
||||
body,
|
||||
button,
|
||||
a,
|
||||
[role="button"],
|
||||
input,
|
||||
label,
|
||||
select,
|
||||
summary,
|
||||
textarea {
|
||||
touch-action: manipulation;
|
||||
}
|
||||
}
|
||||
|
||||
@layer base {
|
||||
|
||||
:root {
|
||||
--background: 0 0% 100%;
|
||||
--foreground: 240 10% 3.9%;
|
||||
|
||||
--card: 0 0% 100%;
|
||||
--card-foreground: 240 10% 3.9%;
|
||||
|
||||
--popover: 0 0% 100%;
|
||||
--popover-foreground: 240 10% 3.9%;
|
||||
|
||||
--primary: 240 5.9% 10%;
|
||||
--primary-foreground: 0 0% 98%;
|
||||
|
||||
--secondary: 240 4.8% 95.9%;
|
||||
--secondary-foreground: 240 5.9% 10%;
|
||||
|
||||
--muted: 240 4.8% 95.9%;
|
||||
--muted-foreground: 240 3.8% 46.1%;
|
||||
|
||||
--accent: 240 4.8% 95.9%;
|
||||
--accent-foreground: 240 5.9% 10%;
|
||||
|
||||
--destructive: 0 84.2% 60.2%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 5.9% 90%;
|
||||
--input: 240 5.9% 90%;
|
||||
--ring: 240 10% 3.9%;
|
||||
|
||||
--radius: 0.5rem;
|
||||
}
|
||||
|
||||
.dark {
|
||||
--background: 240 10% 3.9%;
|
||||
--foreground: 0 0% 98%;
|
||||
|
||||
--card: 240 10% 3.9%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
|
||||
--popover: 240 10% 3.9%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 240 5.9% 10%;
|
||||
|
||||
--secondary: 240 3.7% 15.9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
|
||||
--muted: 240 3.7% 15.9%;
|
||||
--muted-foreground: 240 5% 64.9%;
|
||||
|
||||
--accent: 240 3.7% 15.9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
|
||||
--border: 240 3.7% 15.9%;
|
||||
--input: 240 3.7% 15.9%;
|
||||
--ring: 240 4.9% 83.9%;
|
||||
}
|
||||
|
||||
.amoled {
|
||||
--background: 0 0% 0%;
|
||||
--foreground: 0 0% 98%;
|
||||
--card: 0 0% 0%;
|
||||
--card-foreground: 0 0% 98%;
|
||||
--popover: 0 0% 0%;
|
||||
--popover-foreground: 0 0% 98%;
|
||||
--primary: 0 0% 98%;
|
||||
--primary-foreground: 0 0% 0%;
|
||||
--secondary: 0 0% 9%;
|
||||
--secondary-foreground: 0 0% 98%;
|
||||
--muted: 0 0% 9%;
|
||||
--muted-foreground: 0 0% 60%;
|
||||
--accent: 0 0% 9%;
|
||||
--accent-foreground: 0 0% 98%;
|
||||
--destructive: 0 62.8% 30.6%;
|
||||
--destructive-foreground: 0 0% 98%;
|
||||
--border: 0 0% 12%;
|
||||
--input: 0 0% 12%;
|
||||
--ring: 0 0% 83.9%;
|
||||
}
|
||||
|
||||
* {
|
||||
@apply border-border;
|
||||
}
|
||||
}
|
||||
|
||||
@theme {
|
||||
--color-background: hsl(var(--background));
|
||||
--color-foreground: hsl(var(--foreground));
|
||||
|
||||
--color-card: hsl(var(--card));
|
||||
--color-card-foreground: hsl(var(--card-foreground));
|
||||
|
||||
--color-popover: hsl(var(--popover));
|
||||
--color-popover-foreground: hsl(var(--popover-foreground));
|
||||
|
||||
--color-primary: hsl(var(--primary));
|
||||
--color-primary-foreground: hsl(var(--primary-foreground));
|
||||
|
||||
--color-secondary: hsl(var(--secondary));
|
||||
--color-secondary-foreground: hsl(var(--secondary-foreground));
|
||||
|
||||
--color-muted: hsl(var(--muted));
|
||||
--color-muted-foreground: hsl(var(--muted-foreground));
|
||||
|
||||
--color-accent: hsl(var(--accent));
|
||||
--color-accent-foreground: hsl(var(--accent-foreground));
|
||||
|
||||
--color-destructive: hsl(var(--destructive));
|
||||
--color-destructive-foreground: hsl(var(--destructive-foreground));
|
||||
|
||||
--color-border: hsl(var(--border));
|
||||
--color-input: hsl(var(--input));
|
||||
--color-ring: hsl(var(--ring));
|
||||
|
||||
--radius-lg: var(--radius);
|
||||
--radius-md: calc(var(--radius) - 2px);
|
||||
--radius-sm: calc(var(--radius) - 4px);
|
||||
|
||||
--animate-accordion-down: accordion-down 0.2s ease-out;
|
||||
--animate-accordion-up: accordion-up 0.2s ease-out;
|
||||
|
||||
@keyframes accordion-down {
|
||||
from {
|
||||
height: 0
|
||||
}
|
||||
|
||||
to {
|
||||
height: var(--radix-accordion-content-height)
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes accordion-up {
|
||||
from {
|
||||
height: var(--radix-accordion-content-height)
|
||||
}
|
||||
|
||||
to {
|
||||
height: 0
|
||||
}
|
||||
body {
|
||||
@apply h-full w-full overflow-hidden bg-base-100 text-base-content;
|
||||
}
|
||||
}
|
||||
11
frontend/package-lock.json
generated
11
frontend/package-lock.json
generated
@@ -13,6 +13,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.23",
|
||||
"daisyui": "^5.5.1-beta.2",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18"
|
||||
}
|
||||
@@ -720,6 +721,16 @@
|
||||
],
|
||||
"license": "CC-BY-4.0"
|
||||
},
|
||||
"node_modules/daisyui": {
|
||||
"version": "5.5.1-beta.2",
|
||||
"resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.1-beta.2.tgz",
|
||||
"integrity": "sha512-MJgPmmXKW7G8Vvt/z096Til1vYsB7CCNms5FElU2nDcOxEnjhMTUyQnXgzLjo0C9XWuIXEnr62F95Kv2Udq1GQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/saadeghi/daisyui?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"autoprefixer": "^10.4.23",
|
||||
"daisyui": "^5.5.1-beta.2",
|
||||
"postcss": "^8.5.6",
|
||||
"tailwindcss": "^4.1.18"
|
||||
},
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,239 +1,28 @@
|
||||
use leptos::*;
|
||||
use thaw::*;
|
||||
use shared::{Torrent, AppEvent, TorrentStatus, Theme};
|
||||
use crate::components::toolbar::Toolbar;
|
||||
use crate::components::sidebar::Sidebar;
|
||||
// use crate::components::context_menu::ContextMenu;
|
||||
// use crate::components::modal::Modal;
|
||||
use crate::components::status_bar::StatusBar;
|
||||
use crate::components::torrent_table::TorrentTable;
|
||||
use gloo_net::eventsource::futures::EventSource;
|
||||
use futures::StreamExt;
|
||||
use crate::components::layout::sidebar::Sidebar;
|
||||
use crate::components::layout::toolbar::Toolbar;
|
||||
use crate::components::layout::statusbar::StatusBar;
|
||||
use crate::components::torrent::table::TorrentTable;
|
||||
|
||||
#[component]
|
||||
pub fn App() -> impl IntoView {
|
||||
// Signals
|
||||
let (torrents, set_torrents) = create_signal(Vec::<Torrent>::new());
|
||||
let (sort_key, set_sort_key) = create_signal(6); // 6=Added Date
|
||||
let (sort_asc, set_sort_asc) = create_signal(false); // Descending (Newest first)
|
||||
let (filter_status, set_filter_status) = create_signal(Option::<TorrentStatus>::None);
|
||||
let (active_tab, set_active_tab) = create_signal("torrents");
|
||||
|
||||
// Theme with Persistence
|
||||
let (theme, set_theme) = create_signal({
|
||||
let storage = window().local_storage().ok().flatten();
|
||||
let saved = storage.and_then(|s| s.get_item("vibetorrent_theme").ok().flatten());
|
||||
match saved.as_deref() {
|
||||
Some("Light") => Theme::Light,
|
||||
Some("Amoled") => Theme::Amoled,
|
||||
_ => Theme::Midnight,
|
||||
}
|
||||
});
|
||||
|
||||
// Persist Theme Logic
|
||||
create_effect(move |_| {
|
||||
let val = match theme.get() {
|
||||
Theme::Midnight => "Midnight",
|
||||
Theme::Light => "Light",
|
||||
Theme::Amoled => "Amoled",
|
||||
};
|
||||
|
||||
if let Some(doc) = window().document() {
|
||||
if let Some(body) = doc.body() {
|
||||
let list = body.class_list();
|
||||
// Reset classes
|
||||
let _ = list.remove_1("dark");
|
||||
let _ = list.remove_1("amoled");
|
||||
|
||||
match theme.get() {
|
||||
Theme::Light => {},
|
||||
Theme::Midnight => { let _ = list.add_1("dark"); },
|
||||
Theme::Amoled => {
|
||||
let _ = list.add_1("dark");
|
||||
let _ = list.add_1("amoled");
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(storage) = window().local_storage().ok().flatten() {
|
||||
let _ = storage.set_item("vibetorrent_theme", val);
|
||||
}
|
||||
});
|
||||
|
||||
// Remove Loading Spinner
|
||||
create_effect(move |_| {
|
||||
if let Some(doc) = window().document() {
|
||||
if let Some(el) = doc.get_element_by_id("app-loading") {
|
||||
el.remove();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Debug: Last Updated Timestamp
|
||||
let (last_updated, set_last_updated) = create_signal(0u64);
|
||||
|
||||
// Derived: Filtered & Sorted Logic
|
||||
let processed_torrents = create_memo(move |_| {
|
||||
let mut items = torrents.get();
|
||||
if let Some(status) = filter_status.get() {
|
||||
items.retain(|t| t.status == status);
|
||||
}
|
||||
|
||||
let key = sort_key.get();
|
||||
let asc = sort_asc.get();
|
||||
|
||||
items.sort_by(|a, b| {
|
||||
let cmp = match key {
|
||||
0 => a.name.to_lowercase().cmp(&b.name.to_lowercase()),
|
||||
1 => a.size.cmp(&b.size),
|
||||
2 => a.percent_complete.partial_cmp(&b.percent_complete).unwrap_or(std::cmp::Ordering::Equal),
|
||||
3 => a.down_rate.cmp(&b.down_rate),
|
||||
4 => a.up_rate.cmp(&b.up_rate),
|
||||
5 => a.eta.cmp(&b.eta),
|
||||
6 => a.added_date.cmp(&b.added_date),
|
||||
_ => std::cmp::Ordering::Equal,
|
||||
};
|
||||
if asc { cmp } else { cmp.reverse() }
|
||||
});
|
||||
items
|
||||
});
|
||||
|
||||
// Add Torrent Logic
|
||||
let (show_modal, set_show_modal) = create_signal(false);
|
||||
let (magnet_link, set_magnet_link) = create_signal(String::new());
|
||||
|
||||
let add_torrent = move |_| {
|
||||
spawn_local(async move {
|
||||
let uri = magnet_link.get();
|
||||
if uri.is_empty() { return; }
|
||||
let client = gloo_net::http::Request::post("/api/torrents/add")
|
||||
.header("Content-Type", "application/json")
|
||||
.body(serde_json::to_string(&serde_json::json!({ "uri": uri })).unwrap())
|
||||
.unwrap();
|
||||
if client.send().await.is_ok() {
|
||||
set_magnet_link.set(String::new());
|
||||
set_show_modal.set(false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Connect SSE
|
||||
create_effect(move |_| {
|
||||
spawn_local(async move {
|
||||
let mut es = EventSource::new("/api/events").unwrap();
|
||||
let mut stream = es.subscribe("message").unwrap();
|
||||
|
||||
loop {
|
||||
match stream.next().await {
|
||||
Some(Ok((_, msg))) => {
|
||||
let data = msg.data().as_string().unwrap();
|
||||
if let Ok(event) = serde_json::from_str::<AppEvent>(&data) {
|
||||
match event {
|
||||
AppEvent::FullList(list, ts) => {
|
||||
set_torrents.set(list);
|
||||
set_last_updated.set(ts);
|
||||
}
|
||||
AppEvent::Update(diff) => {
|
||||
set_torrents.update(|list| {
|
||||
if let Some(target) = list.iter_mut().find(|t| t.hash == diff.hash) {
|
||||
if let Some(v) = diff.name { target.name = v; }
|
||||
if let Some(v) = diff.size { target.size = v; }
|
||||
if let Some(v) = diff.down_rate { target.down_rate = v; }
|
||||
if let Some(v) = diff.up_rate { target.up_rate = v; }
|
||||
if let Some(v) = diff.percent_complete { target.percent_complete = v; }
|
||||
if let Some(v) = diff.completed { target.completed = v; }
|
||||
if let Some(v) = diff.eta { target.eta = v; }
|
||||
if let Some(v) = diff.status { target.status = v; }
|
||||
if let Some(v) = diff.error_message { target.error_message = v; }
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None => break,
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Toolbar Callbacks
|
||||
let on_add = Callback::new(move |_| set_show_modal.set(true));
|
||||
let on_start = Callback::new(move |_| logging::log!("Start all - to be implemented with selection"));
|
||||
let on_pause = Callback::new(move |_| logging::log!("Pause all - to be implemented with selection"));
|
||||
let on_delete = Callback::new(move |_| logging::log!("Delete - to be implemented with selection"));
|
||||
let on_settings = Callback::new(move |_| set_active_tab.set(if active_tab.get() == "settings" { "torrents" } else { "settings" }));
|
||||
|
||||
view! {
|
||||
<div class="flex h-screen overflow-hidden bg-background text-foreground font-sans">
|
||||
<Sidebar
|
||||
active_filter=filter_status
|
||||
on_filter_change=Callback::new(move |s| set_filter_status.set(s))
|
||||
/>
|
||||
<div class="flex-1 flex flex-col min-w-0">
|
||||
<Toolbar
|
||||
on_add=on_add
|
||||
on_start=on_start
|
||||
on_pause=on_pause
|
||||
on_delete=on_delete
|
||||
on_settings=on_settings
|
||||
/>
|
||||
<div class="flex flex-col h-screen w-screen overflow-hidden bg-base-100 text-base-content text-sm select-none">
|
||||
// Toolbar at the top
|
||||
<Toolbar />
|
||||
|
||||
{move || if active_tab.get() == "settings" {
|
||||
view! {
|
||||
<div class="p-6">
|
||||
<h1 class="text-2xl font-bold mb-4">"Settings"</h1>
|
||||
<div class="flex gap-4">
|
||||
<Button on_click=move |_| set_theme.set(Theme::Midnight)>"Midnight"</Button>
|
||||
<Button on_click=move |_| set_theme.set(Theme::Light)>"Light"</Button>
|
||||
<Button on_click=move |_| set_theme.set(Theme::Amoled)>"Amoled"</Button>
|
||||
</div>
|
||||
</div>
|
||||
}.into_view()
|
||||
} else {
|
||||
view! { <TorrentTable torrents=processed_torrents /> }.into_view()
|
||||
}}
|
||||
<div class="flex flex-1 overflow-hidden">
|
||||
// Sidebar on the left
|
||||
<Sidebar />
|
||||
|
||||
<StatusBar />
|
||||
// Main Content Area
|
||||
<main class="flex-1 flex flex-col min-w-0 bg-base-100">
|
||||
<TorrentTable />
|
||||
</main>
|
||||
</div>
|
||||
|
||||
// Add Torrent Modal (Inlined)
|
||||
<Show when=move || show_modal.get() fallback=|| ()>
|
||||
<div class="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-end md:items-center justify-center z-[200] animate-in fade-in duration-200 sm:p-4">
|
||||
<div class="bg-card p-6 rounded-t-2xl md:rounded-lg w-full max-w-sm shadow-xl border border-border ring-0 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
||||
<h3 class="text-lg font-semibold text-card-foreground mb-4">"Add Torrent"</h3>
|
||||
|
||||
<div class="space-y-4 mb-6">
|
||||
<p class="text-sm text-muted-foreground">"Paste a magnet link or URL to start downloading."</p>
|
||||
<input
|
||||
type="text"
|
||||
class="w-full bg-input border border-input rounded-md p-2 text-foreground focus:outline-none focus:ring-1 focus:ring-ring"
|
||||
placeholder="magnet:?xt=urn:btih:..."
|
||||
on:input=move |ev| set_magnet_link.set(event_target_value(&ev))
|
||||
prop:value=magnet_link
|
||||
autoFocus
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-3">
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 border border-input bg-background hover:bg-accent hover:text-accent-foreground h-10 px-4 py-2"
|
||||
on:click=move |_| set_show_modal.set(false)
|
||||
>
|
||||
"Cancel"
|
||||
</button>
|
||||
<button
|
||||
class="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
||||
on:click=move |_| add_torrent(())
|
||||
>
|
||||
"Add Download"
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Show>
|
||||
// Status Bar at the bottom
|
||||
<StatusBar />
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
3
frontend/src/components/layout/mod.rs
Normal file
3
frontend/src/components/layout/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
pub mod sidebar;
|
||||
pub mod toolbar;
|
||||
pub mod statusbar;
|
||||
65
frontend/src/components/layout/sidebar.rs
Normal file
65
frontend/src/components/layout/sidebar.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn Sidebar() -> impl IntoView {
|
||||
view! {
|
||||
<aside class="w-64 bg-base-200 h-full flex flex-col border-r border-base-300">
|
||||
<div class="p-4">
|
||||
<h2 class="text-xl font-bold px-4 mb-2 text-primary">"Filters"</h2>
|
||||
<ul class="menu w-full rounded-box gap-1">
|
||||
<li>
|
||||
<a class="active">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||
</svg>
|
||||
"All"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">"12"</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
||||
</svg>
|
||||
"Downloading"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">"4"</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5m-13.5-9L12 3m0 0l4.5 4.5M12 3v13.5" />
|
||||
</svg>
|
||||
"Seeding"
|
||||
<span class="badge badge-sm badge-ghost ml-auto">"8"</span>
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
"Completed"
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
|
||||
</svg>
|
||||
"Inactive"
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div class="mt-auto p-4 border-t border-base-300">
|
||||
<h3 class="text-xs font-bold text-base-content/50 uppercase mb-2 px-4">"Trackers"</h3>
|
||||
<ul class="menu w-full rounded-box gap-1 text-sm">
|
||||
<li><a>"All Trackers"</a></li>
|
||||
<li><a>"Error"</a></li>
|
||||
</ul>
|
||||
</div>
|
||||
</aside>
|
||||
}
|
||||
}
|
||||
35
frontend/src/components/layout/statusbar.rs
Normal file
35
frontend/src/components/layout/statusbar.rs
Normal file
@@ -0,0 +1,35 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn StatusBar() -> impl IntoView {
|
||||
view! {
|
||||
<div class="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">
|
||||
<div class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12.75l3 3m0 0l3-3m-3 3v-7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="font-mono">"0 KB/s"</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 cursor-pointer hover:text-primary transition-colors">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 11.25l-3-3m0 0l-3 3m3-3v7.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<span class="font-mono">"0 KB/s"</span>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-4">
|
||||
<button class="btn btn-ghost btn-xs btn-square" title="Alt Speed Limits">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 6v6h4.5m4.5 0a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="btn btn-ghost btn-xs btn-square" title="Settings">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M9.594 3.94c.09-.542.56-.94 1.11-.94h2.593c.55 0 1.02.398 1.11.94l.213 1.281c.063.374.313.686.645.87.074.04.147.083.22.127.324.196.72.257 1.075.124l1.217-.456a1.125 1.125 0 011.37.49l1.296 2.247a1.125 1.125 0 01-.26 1.431l-1.003.827c-.293.24-.438.613-.431.992a6.759 6.759 0 010 .255c-.007.378.138.75.43.99l1.005.828c.424.35.534.954.26 1.43l-1.298 2.247a1.125 1.125 0 01-1.369.491l-1.217-.456c-.355-.133-.75-.072-1.076.124a6.57 6.57 0 01-.22.128c-.331.183-.581.495-.644.869l-.212 1.28c-.09.543-.56.941-1.11.941h-2.594c-.55 0-1.02-.398-1.11-.94l-.213-1.281c-.062-.374-.312-.686-.644-.87a6.52 6.52 0 01-.22-.127c-.325-.196-.72-.257-1.076-.124l-1.217.456a1.125 1.125 0 01-1.369-.49l-1.297-2.247a1.125 1.125 0 01.26-1.431l1.004-.827c.292-.24.437-.613.43-.992a6.932 6.932 0 010-.255c.007-.378-.138-.75-.43-.99l-1.004-.828a1.125 1.125 0 01-.26-1.43l1.297-2.247a1.125 1.125 0 011.37-.491l1.216.456c.356.133.751.072 1.076-.124.072-.044.146-.087.22-.128.332-.183.582-.495.644-.869l.214-1.281z" />
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
48
frontend/src/components/layout/toolbar.rs
Normal file
48
frontend/src/components/layout/toolbar.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use leptos::*;
|
||||
|
||||
#[component]
|
||||
pub fn Toolbar() -> impl IntoView {
|
||||
view! {
|
||||
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-4">
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm btn-outline gap-2" title="Open Torrent">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||
</svg>
|
||||
"Open"
|
||||
</button>
|
||||
<button class="join-item btn btn-sm btn-outline gap-2" title="Magnet Link">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
|
||||
</svg>
|
||||
"URL"
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm btn-ghost" title="Start">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-success">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M5.25 5.653c0-.856.917-1.398 1.667-.986l11.54 6.348a1.125 1.125 0 010 1.971l-11.54 6.347a1.125 1.125 0 01-1.667-.985V5.653z" />
|
||||
</svg>
|
||||
</button>
|
||||
<button class="join-item btn btn-sm btn-ghost" title="Pause">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5 text-warning">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="join">
|
||||
<button class="join-item btn btn-sm btn-ghost text-error" title="Remove">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M14.74 9l-.346 9m-4.788 0L9.26 9m9.968-3.21c.342.052.682.107 1.022.166m-1.022-.165L18.16 19.673a2.25 2.25 0 01-2.244 2.077H8.084a2.25 2.25 0 01-2.244-2.077L4.772 5.79m14.456 0a48.108 48.108 0 00-3.478-.397m-12 .562c.34-.059.68-.114 1.022-.165m0 0a48.11 48.11 0 013.478-.397m7.5 0v-.916c0-1.18-.91-2.164-2.09-2.201a51.964 51.964 0 00-3.32 0c-1.18.037-2.09 1.022-2.09 2.201v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<input type="text" placeholder="Filter..." class="input input-sm input-bordered w-full max-w-xs" />
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,4 @@
|
||||
pub mod modal;
|
||||
pub mod context_menu;
|
||||
pub mod toolbar;
|
||||
pub mod sidebar;
|
||||
pub mod status_bar;
|
||||
pub mod torrent_table;
|
||||
|
||||
pub mod layout;
|
||||
pub mod torrent;
|
||||
|
||||
@@ -6,7 +6,7 @@ pub fn Modal(
|
||||
children: Children,
|
||||
#[prop(into)] on_confirm: Callback<()>,
|
||||
#[prop(into)] on_cancel: Callback<()>,
|
||||
#[prop(into)] is_open: MaybeSignal<bool>,
|
||||
#[prop(into)] visible: Signal<bool>,
|
||||
#[prop(into, default = "Confirm".to_string())] confirm_text: String,
|
||||
#[prop(into, default = "Cancel".to_string())] cancel_text: String,
|
||||
#[prop(into, default = false)] is_danger: bool,
|
||||
@@ -20,7 +20,7 @@ pub fn Modal(
|
||||
let cancel_text = store_value(cancel_text);
|
||||
|
||||
view! {
|
||||
<Show when=move || is_open.get() fallback=|| ()>
|
||||
<Show when=move || visible.get() fallback=|| ()>
|
||||
<div class="fixed inset-0 bg-background/80 backdrop-blur-sm flex items-end md:items-center justify-center z-[200] animate-in fade-in duration-200 sm:p-4">
|
||||
<div class="bg-card p-6 rounded-t-2xl md:rounded-lg w-full max-w-sm shadow-xl border border-border ring-0 transform transition-all animate-in slide-in-from-bottom-10 md:slide-in-from-bottom-0 md:zoom-in-95">
|
||||
<h3 class="text-lg font-semibold text-card-foreground mb-4">{title.get_value()}</h3>
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
use leptos::*;
|
||||
use thaw::*;
|
||||
use shared::TorrentStatus;
|
||||
|
||||
#[component]
|
||||
pub fn Sidebar(
|
||||
#[prop(into)] active_filter: Signal<Option<TorrentStatus>>,
|
||||
#[prop(into)] on_filter_change: Callback<Option<TorrentStatus>>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="w-64 border-r border-border bg-card/30 flex flex-col">
|
||||
<div class="p-4 font-bold text-lg">"Groups"</div>
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
<Button
|
||||
variant=if active_filter.get().is_none() { ButtonVariant::Primary } else { ButtonVariant::Text }
|
||||
class="w-full justify-start text-left"
|
||||
on_click=move |_| on_filter_change.call(None)
|
||||
>
|
||||
"All"
|
||||
</Button>
|
||||
<Button
|
||||
variant=if active_filter.get() == Some(TorrentStatus::Downloading) { ButtonVariant::Primary } else { ButtonVariant::Text }
|
||||
class="w-full justify-start text-left"
|
||||
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Downloading))
|
||||
>
|
||||
"Downloading"
|
||||
</Button>
|
||||
<Button
|
||||
variant=if active_filter.get() == Some(TorrentStatus::Seeding) { ButtonVariant::Primary } else { ButtonVariant::Text }
|
||||
class="w-full justify-start text-left"
|
||||
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Seeding))
|
||||
>
|
||||
"Seeding"
|
||||
</Button>
|
||||
<Button
|
||||
variant=if active_filter.get() == Some(TorrentStatus::Paused) { ButtonVariant::Primary } else { ButtonVariant::Text }
|
||||
class="w-full justify-start text-left"
|
||||
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Paused))
|
||||
>
|
||||
"Paused"
|
||||
</Button>
|
||||
<Button
|
||||
variant=if active_filter.get() == Some(TorrentStatus::Error) { ButtonVariant::Primary } else { ButtonVariant::Text }
|
||||
class="w-full justify-start text-left"
|
||||
on_click=move |_| on_filter_change.call(Some(TorrentStatus::Error))
|
||||
>
|
||||
"Errors"
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
use leptos::*;
|
||||
use thaw::*;
|
||||
|
||||
#[component]
|
||||
pub fn StatusBar() -> impl IntoView {
|
||||
view! {
|
||||
<div class="h-8 border-t border-border bg-card/30 flex items-center px-4 text-xs space-x-4">
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="i-mdi-arrow-down text-green-500"></span>
|
||||
"0 KB/s"
|
||||
</div>
|
||||
<div class="flex items-center gap-1">
|
||||
<span class="i-mdi-arrow-up text-blue-500"></span>
|
||||
"0 KB/s"
|
||||
</div>
|
||||
<div class="flex-1"></div>
|
||||
<div>"Free Space: 700 GB"</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
use leptos::*;
|
||||
use thaw::*;
|
||||
|
||||
#[component]
|
||||
pub fn Toolbar(
|
||||
#[prop(into)] on_add: Callback<()>,
|
||||
#[prop(into)] on_start: Callback<()>,
|
||||
#[prop(into)] on_pause: Callback<()>,
|
||||
#[prop(into)] on_delete: Callback<()>,
|
||||
#[prop(into)] on_settings: Callback<()>,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex items-center gap-2 p-2 border-b border-border bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<Button variant=ButtonVariant::Text on_click=move |_| on_add.call(())>
|
||||
<span class="i-mdi-plus mr-2"/> "Add"
|
||||
</Button>
|
||||
<div class="h-4 w-px bg-border mx-2"></div>
|
||||
<Button variant=ButtonVariant::Text on_click=move |_| on_start.call(())>
|
||||
<span class="i-mdi-play mr-2"/> "Start"
|
||||
</Button>
|
||||
<Button variant=ButtonVariant::Text on_click=move |_| on_pause.call(())>
|
||||
<span class="i-mdi-pause mr-2"/> "Pause"
|
||||
</Button>
|
||||
<Button variant=ButtonVariant::Text color=ButtonColor::Error on_click=move |_| on_delete.call(())>
|
||||
<span class="i-mdi-delete mr-2"/> "Delete"
|
||||
</Button>
|
||||
<div class="flex-1"></div>
|
||||
<Input placeholder="Filter..." class="w-48" />
|
||||
<Button variant=ButtonVariant::Text on_click=move |_| on_settings.call(())>
|
||||
<span class="i-mdi-cog mr-2"/> "Settings"
|
||||
</Button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
1
frontend/src/components/torrent/mod.rs
Normal file
1
frontend/src/components/torrent/mod.rs
Normal file
@@ -0,0 +1 @@
|
||||
pub mod table;
|
||||
124
frontend/src/components/torrent/table.rs
Normal file
124
frontend/src/components/torrent/table.rs
Normal file
@@ -0,0 +1,124 @@
|
||||
use leptos::*;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Torrent {
|
||||
id: u32,
|
||||
name: String,
|
||||
size: String,
|
||||
progress: f32,
|
||||
status: String,
|
||||
seeds: u32,
|
||||
peers: u32,
|
||||
down_speed: String,
|
||||
up_speed: String,
|
||||
}
|
||||
|
||||
#[component]
|
||||
pub fn TorrentTable() -> impl IntoView {
|
||||
let torrents = vec![
|
||||
Torrent {
|
||||
id: 1,
|
||||
name: "Ubuntu 22.04.3 LTS".to_string(),
|
||||
size: "4.7 GB".to_string(),
|
||||
progress: 100.0,
|
||||
status: "Seeding".to_string(),
|
||||
seeds: 452,
|
||||
peers: 12,
|
||||
down_speed: "0 KB/s".to_string(),
|
||||
up_speed: "1.2 MB/s".to_string(),
|
||||
},
|
||||
Torrent {
|
||||
id: 2,
|
||||
name: "Debian 12.1.0 DVD".to_string(),
|
||||
size: "3.9 GB".to_string(),
|
||||
progress: 45.5,
|
||||
status: "Downloading".to_string(),
|
||||
seeds: 120,
|
||||
peers: 45,
|
||||
down_speed: "4.5 MB/s".to_string(),
|
||||
up_speed: "50 KB/s".to_string(),
|
||||
},
|
||||
Torrent {
|
||||
id: 3,
|
||||
name: "Arch Linux 2023.09.01".to_string(),
|
||||
size: "800 MB".to_string(),
|
||||
progress: 12.0,
|
||||
status: "Downloading".to_string(),
|
||||
seeds: 85,
|
||||
peers: 20,
|
||||
down_speed: "2.1 MB/s".to_string(),
|
||||
up_speed: "10 KB/s".to_string(),
|
||||
},
|
||||
Torrent {
|
||||
id: 4,
|
||||
name: "Fedora Workstation 39".to_string(),
|
||||
size: "2.1 GB".to_string(),
|
||||
progress: 0.0,
|
||||
status: "Paused".to_string(),
|
||||
seeds: 0,
|
||||
peers: 0,
|
||||
down_speed: "0 KB/s".to_string(),
|
||||
up_speed: "0 KB/s".to_string(),
|
||||
},
|
||||
];
|
||||
|
||||
view! {
|
||||
<div class="overflow-x-auto h-full bg-base-100">
|
||||
<table class="table table-xs table-pin-rows w-full max-w-full">
|
||||
<thead>
|
||||
<tr class="bg-base-200 text-base-content/70">
|
||||
<th class="w-8">
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
||||
</label>
|
||||
</th>
|
||||
<th>"Name"</th>
|
||||
<th class="w-24">"Size"</th>
|
||||
<th class="w-48">"Progress"</th>
|
||||
<th class="w-24">"Status"</th>
|
||||
<th class="w-20">"Seeds"</th>
|
||||
<th class="w-20">"Peers"</th>
|
||||
<th class="w-24">"Down Speed"</th>
|
||||
<th class="w-24">"Up Speed"</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{torrents.into_iter().map(|t| {
|
||||
let progress_class = if t.progress == 100.0 { "progress-success" } else { "progress-primary" };
|
||||
let status_class = match t.status.as_str() {
|
||||
"Seeding" => "text-success",
|
||||
"Downloading" => "text-primary",
|
||||
"Paused" => "text-warning",
|
||||
_ => "text-base-content/50"
|
||||
};
|
||||
|
||||
view! {
|
||||
<tr class="hover group border-b border-base-200">
|
||||
<th>
|
||||
<label>
|
||||
<input type="checkbox" class="checkbox checkbox-xs rounded-none" />
|
||||
</label>
|
||||
</th>
|
||||
<td class="font-medium truncate max-w-xs" title={t.name.clone()}>
|
||||
{t.name}
|
||||
</td>
|
||||
<td class="opacity-80 font-mono text-[11px]">{t.size}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-2">
|
||||
<progress class={format!("progress w-24 {}", progress_class)} value={t.progress} max="100"></progress>
|
||||
<span class="text-[10px] opacity-70">{format!("{:.1}%", t.progress)}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class={format!("text-[11px] font-medium {}", status_class)}>{t.status}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80">{t.seeds}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80">{t.peers}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-success">{t.down_speed}</td>
|
||||
<td class="text-right font-mono text-[11px] opacity-80 text-primary">{t.up_speed}</td>
|
||||
</tr>
|
||||
}
|
||||
}).collect::<Vec<_>>()}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
use leptos::*;
|
||||
use thaw::*;
|
||||
use shared::Torrent;
|
||||
|
||||
#[component]
|
||||
pub fn TorrentTable(
|
||||
#[prop(into)] torrents: Signal<Vec<Torrent>>
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
<div class="flex-1 overflow-auto bg-background">
|
||||
<table class="w-full text-left text-xs">
|
||||
<thead class="bg-muted/50 border-b border-border text-muted-foreground font-medium sticky top-0 bg-background z-10">
|
||||
<tr>
|
||||
<th class="px-2 py-1.5 font-medium">"Name"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-20 text-right">"Size"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-24">"Progress"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-20 text-center">"Status"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-20 text-right">"Down"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-20 text-right">"Up"</th>
|
||||
<th class="px-2 py-1.5 font-medium w-20 text-right">"ETA"</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-border">
|
||||
<For
|
||||
each=move || torrents.get()
|
||||
key=|t| t.hash.clone()
|
||||
children=move |torrent| {
|
||||
view! {
|
||||
<tr class="hover:bg-muted/50 group transition-colors">
|
||||
<td class="px-2 py-1.5 truncate max-w-[200px]">{torrent.name}</td>
|
||||
<td class="px-2 py-1.5 text-right whitespace-nowrap text-muted-foreground">{torrent.size}</td>
|
||||
<td class="px-2 py-1.5">
|
||||
<Progress percentage=torrent.percent_complete as f32 />
|
||||
</td>
|
||||
<td class="px-2 py-1.5 text-center">
|
||||
<span class="text-[10px] px-1.5 py-0.5 rounded-full border border-border bg-background">
|
||||
{format!("{:?}", torrent.status)}
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-2 py-1.5 text-right whitespace-nowrap text-blue-500">{torrent.down_rate}</td>
|
||||
<td class="px-2 py-1.5 text-right whitespace-nowrap text-green-500">{torrent.up_rate}</td>
|
||||
<td class="px-2 py-1.5 text-right whitespace-nowrap text-muted-foreground">{torrent.eta}</td>
|
||||
</tr>
|
||||
}
|
||||
}
|
||||
/>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,5 @@ module.exports = {
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user