Compare commits
10 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9447a66cc1 | ||
|
|
45247a020e | ||
|
|
77b77c7775 | ||
|
|
8ef3008cb8 | ||
|
|
ca1dd0caac | ||
|
|
ad336789d9 | ||
|
|
fa248d87ae | ||
|
|
d8a9e9e137 | ||
|
|
ca31b4018f | ||
|
|
7707bfff15 |
146
Cargo.lock
generated
146
Cargo.lock
generated
@@ -1260,15 +1260,24 @@ dependencies = [
|
|||||||
"gloo-timers",
|
"gloo-timers",
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
|
"leptos-shadcn-alert",
|
||||||
"leptos-shadcn-avatar",
|
"leptos-shadcn-avatar",
|
||||||
"leptos-shadcn-badge",
|
"leptos-shadcn-badge",
|
||||||
"leptos-shadcn-button",
|
"leptos-shadcn-button",
|
||||||
"leptos-shadcn-card",
|
"leptos-shadcn-card",
|
||||||
"leptos-shadcn-context-menu",
|
"leptos-shadcn-context-menu",
|
||||||
|
"leptos-shadcn-dialog",
|
||||||
|
"leptos-shadcn-dropdown-menu",
|
||||||
"leptos-shadcn-input",
|
"leptos-shadcn-input",
|
||||||
|
"leptos-shadcn-label",
|
||||||
"leptos-shadcn-progress",
|
"leptos-shadcn-progress",
|
||||||
|
"leptos-shadcn-scroll-area",
|
||||||
"leptos-shadcn-separator",
|
"leptos-shadcn-separator",
|
||||||
"leptos-shadcn-sheet",
|
"leptos-shadcn-sheet",
|
||||||
|
"leptos-shadcn-skeleton",
|
||||||
|
"leptos-shadcn-tabs",
|
||||||
|
"leptos-shadcn-toast",
|
||||||
|
"leptos-shadcn-tooltip",
|
||||||
"leptos-use",
|
"leptos-use",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"log",
|
"log",
|
||||||
@@ -2157,6 +2166,21 @@ dependencies = [
|
|||||||
"send_wrapper",
|
"send_wrapper",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-alert"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "884551df61ade405bdb61b0d5a92a3a88b3a7af7a2d283b8d3e942ae0d71309c"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-avatar"
|
name = "leptos-shadcn-avatar"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2232,6 +2256,36 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-dialog"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a3fdbb636393b150c2db1e37d44a6832e9dde177ce2e81281932fefad8bde98e"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-dropdown-menu"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f50189623a176386a30443d281483c5aa6cc34dc45fa11c3e53bd187ffccde21"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-input"
|
name = "leptos-shadcn-input"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2248,6 +2302,21 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-label"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "5e7cad4b5fae11df9bf3b1d4265a56509a9bb7d3a8580e7487f398b733eadf0c"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-progress"
|
name = "leptos-shadcn-progress"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2263,6 +2332,21 @@ dependencies = [
|
|||||||
"web-sys",
|
"web-sys",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-scroll-area"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "ef3d7bdcae4919ad495529ec2a5974036fb0b959580df310f36b2fd33f90860c"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-shadcn-separator"
|
name = "leptos-shadcn-separator"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
@@ -2307,6 +2391,68 @@ dependencies = [
|
|||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-skeleton"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6c14b6bd0f2fe191e3e114a34cee889fc983546ad488e76e76511e3d75ea3f86"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-tabs"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "39f817c834e70a8359933b7b274564313be64105370611af96f05508541b661b"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-toast"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3315f7ed844e3286704cc7b57db7209cad592c11eee770f5dc48ebdc92d66cfb"
|
||||||
|
dependencies = [
|
||||||
|
"gloo-timers",
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"uuid",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "leptos-shadcn-tooltip"
|
||||||
|
version = "0.8.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "3e41d37932d700444e1d3a21f10f198c3c9e76dde3fd78d58da4b5a099939fd7"
|
||||||
|
dependencies = [
|
||||||
|
"leptos",
|
||||||
|
"leptos-node-ref",
|
||||||
|
"leptos-shadcn-signal-management",
|
||||||
|
"leptos-struct-component",
|
||||||
|
"leptos-style",
|
||||||
|
"tailwind_fuse",
|
||||||
|
"web-sys",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "leptos-struct-component"
|
name = "leptos-struct-component"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|||||||
@@ -43,4 +43,13 @@ leptos-shadcn-context-menu = "0.8"
|
|||||||
leptos-shadcn-separator = "0.8"
|
leptos-shadcn-separator = "0.8"
|
||||||
leptos-shadcn-progress = "0.8"
|
leptos-shadcn-progress = "0.8"
|
||||||
leptos-shadcn-avatar = "0.8"
|
leptos-shadcn-avatar = "0.8"
|
||||||
leptos-shadcn-sheet = "0.8"
|
leptos-shadcn-sheet = "0.8"
|
||||||
|
leptos-shadcn-tabs = "0.8"
|
||||||
|
leptos-shadcn-scroll-area = "0.8"
|
||||||
|
leptos-shadcn-dialog = "0.8"
|
||||||
|
leptos-shadcn-label = "0.8"
|
||||||
|
leptos-shadcn-alert = "0.8"
|
||||||
|
leptos-shadcn-toast = "0.8"
|
||||||
|
leptos-shadcn-dropdown-menu = "0.8"
|
||||||
|
leptos-shadcn-tooltip = "0.8"
|
||||||
|
leptos-shadcn-skeleton = "0.8"
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
@import "tailwindcss";
|
@import "tailwindcss";
|
||||||
@config "./tailwind.config.js";
|
@config "./tailwind.config.js";
|
||||||
|
@source "../src/**/*.rs";
|
||||||
|
@source "/Users/bilal/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/leptos-shadcn-*/src/**/*.rs";
|
||||||
|
|
||||||
@theme {
|
@theme {
|
||||||
/* Shadcn Colors */
|
/* Shadcn Colors */
|
||||||
@@ -41,14 +43,17 @@
|
|||||||
from {
|
from {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@keyframes accordion-up {
|
@keyframes accordion-up {
|
||||||
from {
|
from {
|
||||||
height: var(--radix-accordion-content-height);
|
height: var(--radix-accordion-content-height);
|
||||||
}
|
}
|
||||||
|
|
||||||
to {
|
to {
|
||||||
height: 0;
|
height: 0;
|
||||||
}
|
}
|
||||||
@@ -123,17 +128,35 @@
|
|||||||
* {
|
* {
|
||||||
@apply border-border;
|
@apply border-border;
|
||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
@apply bg-background text-foreground;
|
@apply bg-background text-foreground;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Ensure Shadcn Utilities are always available */
|
/* Ensure Shadcn Utilities are always available */
|
||||||
.bg-popover { background-color: hsl(var(--popover)); }
|
.bg-popover {
|
||||||
.text-popover-foreground { color: hsl(var(--popover-foreground)); }
|
background-color: hsl(var(--popover));
|
||||||
.border-border { border-color: hsl(var(--border)); }
|
}
|
||||||
.shadow-md { box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1); }
|
|
||||||
.z-50 { z-index: 50; }
|
.text-popover-foreground {
|
||||||
.z-100 { z-index: 100; }
|
color: hsl(var(--popover-foreground));
|
||||||
|
}
|
||||||
|
|
||||||
|
.border-border {
|
||||||
|
border-color: hsl(var(--border));
|
||||||
|
}
|
||||||
|
|
||||||
|
.shadow-md {
|
||||||
|
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-50 {
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.z-100 {
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Fix for iOS click/blur events */
|
/* Fix for iOS click/blur events */
|
||||||
@@ -151,4 +174,4 @@
|
|||||||
|
|
||||||
:focus {
|
:focus {
|
||||||
outline: none !important;
|
outline: none !important;
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,12 +1,14 @@
|
|||||||
use crate::components::layout::protected::Protected;
|
use crate::components::layout::protected::Protected;
|
||||||
use crate::components::toast::ToastContainer;
|
use crate::components::toast::ToastContainer;
|
||||||
use crate::components::torrent::table::TorrentTable;
|
use crate::components::torrent::table::TorrentTable;
|
||||||
|
use crate::components::torrent::detail::TorrentDetail;
|
||||||
use crate::components::auth::login::Login;
|
use crate::components::auth::login::Login;
|
||||||
use crate::components::auth::setup::Setup;
|
use crate::components::auth::setup::Setup;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
use leptos_router::components::{Router, Routes, Route};
|
use leptos_router::components::{Router, Routes, Route};
|
||||||
use leptos_router::hooks::use_navigate;
|
use leptos_router::hooks::use_navigate;
|
||||||
|
use leptos_shadcn_skeleton::Skeleton;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn App() -> impl IntoView {
|
pub fn App() -> impl IntoView {
|
||||||
@@ -115,16 +117,51 @@ pub fn App() -> impl IntoView {
|
|||||||
|
|
||||||
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-background">
|
<div class="flex h-screen bg-background">
|
||||||
<div class="flex flex-col items-center gap-4">
|
// Sidebar skeleton
|
||||||
<div class="animate-spin h-8 w-8 border-4 border-primary border-t-transparent rounded-full"></div>
|
<div class="w-56 border-r border-border p-4 space-y-4">
|
||||||
<p class="text-sm text-muted-foreground">"Yükleniyor..."</p>
|
<Skeleton class="h-8 w-3/4" />
|
||||||
|
<div class="space-y-2">
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-4/5" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
<Skeleton class="h-6 w-3/5" />
|
||||||
|
<Skeleton class="h-6 w-full" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
// Main content skeleton
|
||||||
|
<div class="flex-1 flex flex-col">
|
||||||
|
// Header skeleton
|
||||||
|
<div class="border-b border-border p-4 flex items-center gap-4">
|
||||||
|
<Skeleton class="h-8 w-48" />
|
||||||
|
<Skeleton class="h-8 w-64" />
|
||||||
|
<div class="ml-auto"><Skeleton class="h-8 w-24" /></div>
|
||||||
|
</div>
|
||||||
|
// Table skeleton rows
|
||||||
|
<div class="flex-1 p-4 space-y-3">
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-full" />
|
||||||
|
<Skeleton class="h-10 w-3/4" />
|
||||||
|
</div>
|
||||||
|
// Status bar skeleton
|
||||||
|
<div class="border-t border-border p-3">
|
||||||
|
<Skeleton class="h-5 w-96" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}.into_any()>
|
}.into_any()>
|
||||||
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
<Show when=move || is_authenticated.0.get() fallback=|| ()>
|
||||||
<Protected>
|
<Protected>
|
||||||
<TorrentTable />
|
<div class="flex flex-col h-full overflow-hidden">
|
||||||
|
<div class="flex-1 overflow-hidden">
|
||||||
|
<TorrentTable />
|
||||||
|
</div>
|
||||||
|
<TorrentDetail />
|
||||||
|
</div>
|
||||||
</Protected>
|
</Protected>
|
||||||
</Show>
|
</Show>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_card::{Card, CardHeader, CardContent};
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::Button;
|
||||||
|
use leptos_shadcn_label::Label;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Login() -> impl IntoView {
|
pub fn Login() -> impl IntoView {
|
||||||
@@ -32,8 +37,8 @@ pub fn Login() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||||
<div class="w-full max-w-sm rounded-xl border border-border bg-card text-card-foreground shadow-lg">
|
<Card class="w-full max-w-sm shadow-lg">
|
||||||
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center">
|
<CardHeader class="pb-2 items-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-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm 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-6 h-6">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.362 5.214A8.252 8.252 0 0112 21 8.25 8.25 0 016.038 7.048 8.287 8.287 0 009 9.6a8.983 8.983 0 013.361-6.867 8.21 8.25 0 003 2.48z" />
|
||||||
@@ -42,56 +47,53 @@ pub fn Login() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent"</h3>
|
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent"</h3>
|
||||||
<p class="text-sm text-muted-foreground">"Hesabınıza giriş yapın"</p>
|
<p class="text-sm text-muted-foreground">"Hesabınıza giriş yapın"</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
|
||||||
<div class="p-6 pt-4">
|
<CardContent class="pt-4">
|
||||||
<form on:submit=handle_login class="space-y-4">
|
<form on:submit=handle_login class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none">"Kullanıcı Adı"</label>
|
<Label>"Kullanıcı Adı"</Label>
|
||||||
<input
|
<Input
|
||||||
type="text"
|
input_type="text"
|
||||||
placeholder="Kullanıcı adınız"
|
placeholder="Kullanıcı adınız"
|
||||||
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"
|
value=MaybeProp::derive(move || Some(username.0.get()))
|
||||||
prop:value=move || username.0.get()
|
on_change=Callback::new(move |val: String| username.1.set(val))
|
||||||
on:input=move |ev| username.1.set(event_target_value(&ev))
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none">"Şifre"</label>
|
<Label>"Şifre"</Label>
|
||||||
<input
|
<Input
|
||||||
type="password"
|
input_type="password"
|
||||||
placeholder="******"
|
placeholder="******"
|
||||||
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"
|
value=MaybeProp::derive(move || Some(password.0.get()))
|
||||||
prop:value=move || password.0.get()
|
on_change=Callback::new(move |val: String| password.1.set(val))
|
||||||
on:input=move |ev| password.1.set(event_target_value(&ev))
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when=move || error.0.get().is_some()>
|
<Show when=move || error.0.get().is_some()>
|
||||||
<div class="rounded-md border border-destructive/50 bg-destructive/10 p-3 text-sm text-destructive">
|
<Alert variant=AlertVariant::Destructive>
|
||||||
{move || error.0.get().unwrap_or_default()}
|
<AlertDescription>
|
||||||
</div>
|
{move || error.0.get().unwrap_or_default()}
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button
|
<Button
|
||||||
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"
|
class="w-full"
|
||||||
type="submit"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
disabled=move || loading.0.get()
|
|
||||||
>
|
>
|
||||||
<Show when=move || loading.0.get() fallback=|| "Giriş Yap">
|
<Show when=move || loading.0.get() fallback=|| "Giriş Yap">
|
||||||
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
"Giriş Yapılıyor..."
|
"Giriş Yapılıyor..."
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,10 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_card::{Card, CardHeader, CardContent};
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::Button;
|
||||||
|
use leptos_shadcn_label::Label;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Setup() -> impl IntoView {
|
pub fn Setup() -> impl IntoView {
|
||||||
@@ -48,8 +53,8 @@ pub fn Setup() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
<div class="flex items-center justify-center min-h-screen bg-muted/40 px-4">
|
||||||
<div class="w-full max-w-md rounded-xl border border-border bg-card text-card-foreground shadow-lg overflow-hidden">
|
<Card class="w-full max-w-md shadow-lg overflow-hidden">
|
||||||
<div class="flex flex-col space-y-1.5 p-6 pb-2 items-center text-center">
|
<CardHeader class="pb-2 items-center 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-12 h-12 bg-primary rounded-xl flex items-center justify-center text-primary-foreground shadow-sm 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-6 h-6">
|
||||||
<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" />
|
||||||
@@ -57,74 +62,63 @@ pub fn Setup() -> impl IntoView {
|
|||||||
</div>
|
</div>
|
||||||
<h3 class="font-semibold tracking-tight text-2xl">"VibeTorrent Kurulumu"</h3>
|
<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>
|
<p class="text-sm text-muted-foreground">"Yönetici hesabınızı oluşturun"</p>
|
||||||
</div>
|
</CardHeader>
|
||||||
|
|
||||||
<div class="p-6 pt-4">
|
<CardContent class="pt-4">
|
||||||
<form on:submit=handle_setup class="space-y-4">
|
<form on:submit=handle_setup class="space-y-4">
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Yönetici Kullanıcı Adı"</Label>
|
||||||
"Yönetici Kullanıcı Adı"
|
<Input
|
||||||
</label>
|
input_type="text"
|
||||||
<input
|
placeholder="admin"
|
||||||
type="text"
|
value=MaybeProp::derive(move || Some(username.0.get()))
|
||||||
placeholder="admin"
|
on_change=Callback::new(move |val: String| username.1.set(val))
|
||||||
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"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || username.0.get()
|
|
||||||
on:input=move |ev| username.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Şifre"</Label>
|
||||||
"Şifre"
|
<Input
|
||||||
</label>
|
input_type="password"
|
||||||
<input
|
placeholder="******"
|
||||||
type="password"
|
value=MaybeProp::derive(move || Some(password.0.get()))
|
||||||
placeholder="******"
|
on_change=Callback::new(move |val: String| password.1.set(val))
|
||||||
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"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || password.0.get()
|
|
||||||
on:input=move |ev| password.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<label class="text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70">
|
<Label>"Şifre Onay"</Label>
|
||||||
"Şifre Onay"
|
<Input
|
||||||
</label>
|
input_type="password"
|
||||||
<input
|
placeholder="******"
|
||||||
type="password"
|
value=MaybeProp::derive(move || Some(confirm_password.0.get()))
|
||||||
placeholder="******"
|
on_change=Callback::new(move |val: String| confirm_password.1.set(val))
|
||||||
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"
|
disabled=Signal::derive(move || loading.0.get())
|
||||||
prop:value=move || confirm_password.0.get()
|
|
||||||
on:input=move |ev| confirm_password.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || loading.0.get()
|
|
||||||
required
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
<Show when=move || error.0.get().is_some() fallback=|| ()>
|
||||||
<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">
|
<Alert variant=AlertVariant::Destructive>
|
||||||
<span>{move || error.0.get().unwrap_or_default()}</span>
|
<AlertDescription>
|
||||||
</div>
|
<span>{move || error.0.get().unwrap_or_default()}</span>
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
<button
|
<Button
|
||||||
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"
|
class="w-full"
|
||||||
type="submit"
|
disabled=Signal::derive(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="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
"Kuruluyor..."
|
"Kuruluyor..."
|
||||||
</Show>
|
</Show>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</CardContent>
|
||||||
</div>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::portal::Portal;
|
use web_sys::MouseEvent;
|
||||||
use leptos_shadcn_context_menu::{
|
use wasm_bindgen::prelude::*;
|
||||||
ContextMenu,
|
use wasm_bindgen::JsCast;
|
||||||
ContextMenuContent,
|
|
||||||
ContextMenuItem,
|
// ── Kendi reaktif Context Menu implementasyonumuz ──
|
||||||
ContextMenuTrigger,
|
// leptos-shadcn-context-menu v0.8.1'de ContextMenuContent'te
|
||||||
ContextMenuSeparator,
|
// `if open.get()` statik kontrolü reaktif değil. Aşağıda
|
||||||
};
|
// `Show` bileşeni ile düzgün reaktif versiyon yer alıyor.
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn TorrentContextMenu(
|
pub fn TorrentContextMenu(
|
||||||
@@ -17,68 +17,142 @@ pub fn TorrentContextMenu(
|
|||||||
let hash = StoredValue::new(torrent_hash);
|
let hash = StoredValue::new(torrent_hash);
|
||||||
let on_action = StoredValue::new(on_action);
|
let on_action = StoredValue::new(on_action);
|
||||||
|
|
||||||
|
let open = RwSignal::new(false);
|
||||||
|
let position = RwSignal::new((0i32, 0i32));
|
||||||
|
|
||||||
|
// Sağ tıklama handler
|
||||||
|
let on_contextmenu = move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
position.set((e.client_x(), e.client_y()));
|
||||||
|
open.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Menü dışına tıklandığında kapanma
|
||||||
|
Effect::new(move |_| {
|
||||||
|
if open.get() {
|
||||||
|
let cb = Closure::wrap(Box::new(move |_: MouseEvent| {
|
||||||
|
open.set(false);
|
||||||
|
}) as Box<dyn Fn(MouseEvent)>);
|
||||||
|
|
||||||
|
let window = web_sys::window().unwrap();
|
||||||
|
let document = window.document().unwrap();
|
||||||
|
let _ = document.add_event_listener_with_callback(
|
||||||
|
"click",
|
||||||
|
cb.as_ref().unchecked_ref(),
|
||||||
|
);
|
||||||
|
|
||||||
|
// Cleanup: tek sefer dinleyici — click yakalandığında otomatik kapanıp listener kalıyor
|
||||||
|
// ama open=false olduğunda effect tekrar çalışmaz, böylece sorun yok.
|
||||||
|
cb.forget();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let menu_action = move |action: &'static str| {
|
||||||
|
open.set(false);
|
||||||
|
on_action.get_value().run((action.to_string(), hash.get_value()));
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<ContextMenu>
|
<div
|
||||||
<ContextMenuTrigger class="w-full">
|
class="w-full"
|
||||||
{children()}
|
on:contextmenu=on_contextmenu
|
||||||
</ContextMenuTrigger>
|
>
|
||||||
|
{children()}
|
||||||
<Portal>
|
</div>
|
||||||
<ContextMenuContent class="w-56 z-[100] bg-popover border border-border shadow-md rounded-md p-1">
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
|
||||||
on_action.get_value().run(("start".to_string(), hash.get_value()));
|
|
||||||
}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
|
||||||
<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>
|
|
||||||
"Start"
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
|
||||||
on_action.get_value().run(("stop".to_string(), hash.get_value()));
|
|
||||||
}>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
|
||||||
</svg>
|
|
||||||
"Stop"
|
|
||||||
</ContextMenuItem>
|
|
||||||
|
|
||||||
<ContextMenuItem on:click=move |_| {
|
<Show when=move || open.get()>
|
||||||
on_action.get_value().run(("recheck".to_string(), hash.get_value()));
|
{
|
||||||
}>
|
let (x, y) = position.get();
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
// Menü yaklaşık boyutları
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
let menu_width = 200;
|
||||||
</svg>
|
let menu_height = 220;
|
||||||
"Recheck"
|
let window = web_sys::window().unwrap();
|
||||||
</ContextMenuItem>
|
let vw = window.inner_width().unwrap().as_f64().unwrap() as i32;
|
||||||
|
let vh = window.inner_height().unwrap().as_f64().unwrap() as i32;
|
||||||
<ContextMenuSeparator />
|
// Sağa taşarsa sola aç, alta taşarsa yukarı aç
|
||||||
|
let final_x = if x + menu_width > vw { x - menu_width } else { x };
|
||||||
<ContextMenuItem
|
let final_y = if y + menu_height > vh { y - menu_height } else { y };
|
||||||
class="text-destructive focus:text-destructive-foreground focus:bg-destructive"
|
let final_x = final_x.max(0);
|
||||||
on:click=move |_| {
|
let final_y = final_y.max(0);
|
||||||
on_action.get_value().run(("delete".to_string(), hash.get_value()));
|
view! {
|
||||||
}
|
<div
|
||||||
|
class="fixed inset-0 z-[99]"
|
||||||
|
on:click=move |e: MouseEvent| {
|
||||||
|
e.stop_propagation();
|
||||||
|
open.set(false);
|
||||||
|
}
|
||||||
|
on:contextmenu=move |e: MouseEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
e.stop_propagation();
|
||||||
|
open.set(false);
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
class="fixed z-[100] min-w-[12rem] overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95"
|
||||||
|
style=format!("left: {}px; top: {}px;", final_x, final_y)
|
||||||
|
on:click=move |e: MouseEvent| e.stop_propagation()
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
// Start
|
||||||
<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.164h-2.34c-1.18 0-2.09.984-2.09 2.164v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
<div
|
||||||
</svg>
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
"Remove"
|
on:click=move |_| menu_action("start")
|
||||||
</ContextMenuItem>
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<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>
|
||||||
|
"Start"
|
||||||
|
</div>
|
||||||
|
|
||||||
<ContextMenuItem
|
// Stop
|
||||||
class="text-destructive focus:text-destructive-foreground focus:bg-destructive"
|
<div
|
||||||
on:click=move |_| {
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
on_action.get_value().run(("delete_with_data".to_string(), hash.get_value()));
|
on:click=move |_| menu_action("stop")
|
||||||
}
|
>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25-2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
</svg>
|
||||||
</svg>
|
"Stop"
|
||||||
"Remove Data"
|
</div>
|
||||||
</ContextMenuItem>
|
|
||||||
</ContextMenuContent>
|
// Recheck
|
||||||
</Portal>
|
<div
|
||||||
</ContextMenu>
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors hover:bg-accent hover:text-accent-foreground"
|
||||||
|
on:click=move |_| menu_action("recheck")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M16.023 9.348h4.992v-.001M2.985 19.644v-4.992m0 0h4.992m-4.993 0l3.181 3.183a8.25 8.25 0 0013.803-3.7M4.031 9.865a8.25 8.25 0 0113.803-3.7l3.181 3.182m0-4.991v4.99" />
|
||||||
|
</svg>
|
||||||
|
"Recheck"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Separator
|
||||||
|
<div class="-mx-1 my-1 h-px bg-border" />
|
||||||
|
|
||||||
|
// Remove
|
||||||
|
<div
|
||||||
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
on:click=move |_| menu_action("delete")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<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.164h-2.34c-1.18 0-2.09.984-2.09 2.164v.916m7.5 0a48.667 48.667 0 00-7.5 0" />
|
||||||
|
</svg>
|
||||||
|
"Remove"
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Remove with Data
|
||||||
|
<div
|
||||||
|
class="relative flex cursor-pointer select-none items-center rounded-sm px-2 py-1.5 text-sm outline-none transition-colors text-destructive hover:bg-destructive hover:text-destructive-foreground"
|
||||||
|
on:click=move |_| menu_action("delete_with_data")
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="mr-2 h-4 w-4 opacity-70">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5m6 4.125l2.25 2.25m0 0l2.25 2.25M12 13.875l2.25-2.25M12 13.875l-2.25-2.25M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />
|
||||||
|
</svg>
|
||||||
|
"Remove with Data"
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</Show>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,8 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
||||||
|
use leptos_shadcn_avatar::{Avatar, AvatarFallback};
|
||||||
|
use leptos_shadcn_separator::Separator;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Sidebar() -> impl IntoView {
|
pub fn Sidebar() -> impl IntoView {
|
||||||
@@ -54,23 +57,7 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
is_mobile_menu_open.set(false);
|
is_mobile_menu_open.set(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
let filter_class = move |f: crate::store::FilterStatus| {
|
let is_active = move |f: crate::store::FilterStatus| store.filter.get() == f;
|
||||||
let base = "w-full justify-start gap-2 h-9 px-4 py-2 inline-flex items-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";
|
|
||||||
if store.filter.get() == f {
|
|
||||||
format!("{} bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", base)
|
|
||||||
} else {
|
|
||||||
format!("{} hover:bg-accent hover:text-accent-foreground text-muted-foreground", base)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let handle_logout = move |_| {
|
|
||||||
spawn_local(async move {
|
|
||||||
if shared::server_fns::auth::logout().await.is_ok() {
|
|
||||||
let window = web_sys::window().expect("window should exist");
|
|
||||||
let _ = window.location().set_href("/login");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
let username = move || {
|
let username = move || {
|
||||||
store.user.get().unwrap_or_else(|| "User".to_string())
|
store.user.get().unwrap_or_else(|| "User".to_string())
|
||||||
@@ -89,76 +76,116 @@ pub fn Sidebar() -> impl IntoView {
|
|||||||
<div class="space-y-1">
|
<div class="space-y-1">
|
||||||
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
<h4 class="mb-1 rounded-md px-2 py-1 text-sm font-semibold text-muted-foreground">"Filters"</h4>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::All)} on:click=move |_| set_filter(crate::store::FilterStatus::All)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::All) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::All))
|
||||||
|
>
|
||||||
|
<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="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
|
||||||
</svg>
|
</svg>
|
||||||
"All"
|
"All"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{total_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{total_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Downloading)} on:click=move |_| set_filter(crate::store::FilterStatus::Downloading)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Downloading) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Downloading))
|
||||||
|
>
|
||||||
|
<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="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" />
|
<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>
|
</svg>
|
||||||
"Downloading"
|
"Downloading"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{downloading_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{downloading_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Seeding)} on:click=move |_| set_filter(crate::store::FilterStatus::Seeding)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Seeding) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Seeding))
|
||||||
|
>
|
||||||
|
<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="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" />
|
<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>
|
</svg>
|
||||||
"Seeding"
|
"Seeding"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{seeding_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{seeding_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Completed)} on:click=move |_| set_filter(crate::store::FilterStatus::Completed)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Completed) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Completed))
|
||||||
|
>
|
||||||
|
<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.75L11.25 15 15 9.75M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
<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>
|
</svg>
|
||||||
"Completed"
|
"Completed"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{completed_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{completed_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Paused)} on:click=move |_| set_filter(crate::store::FilterStatus::Paused)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Paused) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Paused))
|
||||||
|
>
|
||||||
|
<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.75 5.25v13.5m-7.5-13.5v13.5" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 5.25v13.5m-7.5-13.5v13.5" />
|
||||||
</svg>
|
</svg>
|
||||||
"Paused"
|
"Paused"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{paused_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{paused_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<button class={move || filter_class(crate::store::FilterStatus::Inactive)} on:click=move |_| set_filter(crate::store::FilterStatus::Inactive)>
|
<Button
|
||||||
<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 mr-2">
|
variant=MaybeProp::derive(move || Some(if is_active(crate::store::FilterStatus::Inactive) { ButtonVariant::Secondary } else { ButtonVariant::Ghost }))
|
||||||
|
size=ButtonSize::Sm
|
||||||
|
class="w-full justify-start gap-2"
|
||||||
|
on_click=Callback::new(move |()| set_filter(crate::store::FilterStatus::Inactive))
|
||||||
|
>
|
||||||
|
<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="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" />
|
<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>
|
</svg>
|
||||||
"Inactive"
|
"Inactive"
|
||||||
<span class="ml-auto text-xs font-mono opacity-70">{inactive_count}</span>
|
<span class="ml-auto text-xs font-mono opacity-70">{inactive_count}</span>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="p-4 border-t border-border bg-card">
|
<Separator />
|
||||||
|
|
||||||
|
<div class="p-4 bg-card">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
<div class="relative flex h-8 w-8 shrink-0 overflow-hidden rounded-full bg-muted">
|
<Avatar class="h-8 w-8">
|
||||||
<span class="flex h-full w-full items-center justify-center rounded-full bg-primary text-primary-foreground text-xs font-medium">
|
<AvatarFallback class="bg-primary text-primary-foreground text-xs font-medium">
|
||||||
{first_letter}
|
{first_letter}
|
||||||
</span>
|
</AvatarFallback>
|
||||||
</div>
|
</Avatar>
|
||||||
<div class="flex-1 overflow-hidden">
|
<div class="flex-1 overflow-hidden">
|
||||||
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
<div class="font-medium text-sm truncate text-foreground">{username}</div>
|
||||||
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
<div class="text-[10px] text-muted-foreground truncate">"Online"</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<Button
|
||||||
class="inline-flex items-center justify-center whitespace-nowrap 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 hover:bg-accent hover:text-accent-foreground h-8 w-8 text-destructive"
|
variant=ButtonVariant::Ghost
|
||||||
title="Logout"
|
size=ButtonSize::Icon
|
||||||
on:click=handle_logout
|
class="text-destructive h-8 w-8"
|
||||||
|
on_click=Callback::new(move |()| {
|
||||||
|
spawn_local(async move {
|
||||||
|
if shared::server_fns::auth::logout().await.is_ok() {
|
||||||
|
let window = web_sys::window().expect("window should exist");
|
||||||
|
let _ = window.location().set_href("/login");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
>
|
>
|
||||||
<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">
|
<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.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 9V5.25A2.25 2.25 0 0013.5 3h-6a2.25 2.25 0 00-2.25 2.25v13.5A2.25 2.25 0 007.5 21h6a2.25 2.25 0 002.25-2.25V15M12 9l-3 3m0 0l3 3m-3-3h12.75" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::{Button, ButtonVariant, ButtonSize};
|
||||||
use crate::components::torrent::add_torrent::AddTorrentDialog;
|
use crate::components::torrent::add_torrent::AddTorrentDialog;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -9,54 +11,51 @@ pub fn Toolbar() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
|
<div class="flex min-h-14 h-auto items-center border-b border-border bg-background px-4" style="padding-top: env(safe-area-inset-top);">
|
||||||
<div class="flex flex-1 items-center gap-4">
|
// Sol kısım: Menü butonu + Add Torrent
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
// Mobile Menu Trigger
|
// Mobile Menu Trigger
|
||||||
<button
|
<Button
|
||||||
on:click=move |_| is_mobile_menu_open.update(|v| *v = !*v)
|
variant=ButtonVariant::Ghost
|
||||||
class="lg:hidden 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 hover:bg-accent hover:text-accent-foreground h-10 w-10"
|
size=ButtonSize::Icon
|
||||||
|
class="lg:hidden"
|
||||||
|
on_click=Callback::new(move |()| is_mobile_menu_open.update(|v| *v = !*v))
|
||||||
>
|
>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" class="w-5 h-5 stroke-current"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg>
|
||||||
</button>
|
</Button>
|
||||||
|
|
||||||
<div class="flex items-center gap-3">
|
<Button
|
||||||
<button
|
class="gap-2 shadow"
|
||||||
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-9 px-4 py-2 shadow gap-2"
|
on_click=Callback::new(move |()| show_add_modal.1.set(true))
|
||||||
on:click=move |_| show_add_modal.1.set(true)
|
>
|
||||||
>
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5">
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4 md:w-5 md:h-5">
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 4.5v15m7.5-7.5h-15" />
|
</svg>
|
||||||
|
<span class="hidden sm:inline">"Add Torrent"</span>
|
||||||
|
<span class="sm:hidden">"Add"</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Sağ kısım: Search kutusu
|
||||||
|
<div class="flex flex-1 items-center justify-end gap-2">
|
||||||
|
<div class="hidden md:flex items-center gap-2 w-full max-w-xs">
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="absolute left-2.5 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="m21 21-5.197-5.197m0 0A7.5 7.5 0 1 0 5.196 5.196a7.5 7.5 0 0 0 10.607 10.607Z" />
|
||||||
</svg>
|
</svg>
|
||||||
<span class="hidden sm:inline">"Add Torrent"</span>
|
<Input
|
||||||
<span class="sm:hidden">"Add"</span>
|
input_type="search"
|
||||||
</button>
|
placeholder="Search..."
|
||||||
|
value=MaybeProp::derive(move || Some(store.search_query.get()))
|
||||||
|
on_change=Callback::new(move |val: String| store.search_query.set(val))
|
||||||
|
class="pl-8 h-9"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="hidden md:flex items-center justify-center flex-1">
|
<Show when=move || show_add_modal.0.get()>
|
||||||
<div class="relative w-full max-w-sm">
|
<AddTorrentDialog on_close=Callback::new(move |()| show_add_modal.1.set(false)) />
|
||||||
<input
|
</Show>
|
||||||
type="text"
|
|
||||||
placeholder="Search..."
|
|
||||||
class="flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50"
|
|
||||||
prop:value=move || store.search_query.get()
|
|
||||||
on:input=move |ev| store.search_query.set(event_target_value(&ev))
|
|
||||||
/>
|
|
||||||
<Show when=move || !store.search_query.get().is_empty()>
|
|
||||||
<button
|
|
||||||
class="absolute right-2 top-1/2 -translate-y-1/2 inline-flex items-center justify-center rounded-full text-xs font-medium hover:bg-muted h-5 w-5 opacity-50 hover:opacity-100 transition-opacity"
|
|
||||||
on:click=move |_| store.search_query.set(String::new())
|
|
||||||
>
|
|
||||||
"×"
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-1 justify-end px-4 gap-2">
|
|
||||||
<Show when=move || show_add_modal.0.get()>
|
|
||||||
<AddTorrentDialog on_close=Callback::new(move |()| show_add_modal.1.set(false)) />
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,30 +1,22 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use shared::NotificationLevel;
|
use shared::NotificationLevel;
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertVariant};
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Toast Components - Shadcn Style
|
// Toast Components - Using ShadCN Alert
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
|
|
||||||
/// Returns the Shadcn class for the notification level
|
fn level_to_variant(level: &NotificationLevel) -> AlertVariant {
|
||||||
fn get_toast_class(level: &NotificationLevel) -> &'static str {
|
|
||||||
match level {
|
match level {
|
||||||
NotificationLevel::Info => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-background text-foreground border-border",
|
NotificationLevel::Info => AlertVariant::Default,
|
||||||
NotificationLevel::Success => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-background text-foreground border-primary/50 text-primary",
|
NotificationLevel::Success => AlertVariant::Success,
|
||||||
NotificationLevel::Warning => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all bg-yellow-50 dark:bg-yellow-900/20 text-yellow-600 dark:text-yellow-500 border-yellow-200 dark:border-yellow-900",
|
NotificationLevel::Warning => AlertVariant::Warning,
|
||||||
NotificationLevel::Error => "group pointer-events-auto relative flex w-full items-center justify-between space-x-4 overflow-hidden rounded-md border p-4 shadow-lg transition-all destructive group border-destructive bg-destructive text-destructive-foreground",
|
NotificationLevel::Error => AlertVariant::Destructive,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Individual toast item component
|
fn level_icon(level: &NotificationLevel) -> impl IntoView {
|
||||||
#[component]
|
match level {
|
||||||
fn ToastItem(
|
|
||||||
level: NotificationLevel,
|
|
||||||
message: String,
|
|
||||||
) -> impl IntoView {
|
|
||||||
let toast_class = get_toast_class(&level);
|
|
||||||
|
|
||||||
// Icons
|
|
||||||
let icon_svg = match level {
|
|
||||||
NotificationLevel::Info => view! {
|
NotificationLevel::Info => view! {
|
||||||
<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 opacity-90">
|
<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 opacity-90">
|
||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M11.25 11.25l.041-.02a.75.75 0 011.063.852l-.708 2.836a.75.75 0 001.063.853l.041-.021M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-9-3.75h.008v.008H12V8.25z" />
|
||||||
@@ -45,15 +37,25 @@ fn ToastItem(
|
|||||||
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m9-.75a9 9 0 11-18 0 9 9 0 0118 0zm-9 3.75h.008v.008H12v-.008z" />
|
||||||
</svg>
|
</svg>
|
||||||
}.into_any(),
|
}.into_any(),
|
||||||
};
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Individual toast item component
|
||||||
|
#[component]
|
||||||
|
fn ToastItem(
|
||||||
|
level: NotificationLevel,
|
||||||
|
message: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let variant = level_to_variant(&level);
|
||||||
|
let icon = level_icon(&level);
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class=toast_class>
|
<Alert variant=variant class="pointer-events-auto shadow-lg">
|
||||||
<div class="flex items-center gap-3">
|
<div class="flex items-center gap-3">
|
||||||
{icon_svg}
|
{icon}
|
||||||
<div class="text-sm font-medium">{message}</div>
|
<div class="text-sm font-medium">{message}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Alert>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::html;
|
|
||||||
use leptos::task::spawn_local;
|
use leptos::task::spawn_local;
|
||||||
|
use leptos_shadcn_input::Input;
|
||||||
|
use leptos_shadcn_button::{Button, ButtonVariant};
|
||||||
|
use leptos_shadcn_alert::{Alert, AlertDescription, AlertVariant};
|
||||||
use crate::store::TorrentStore;
|
use crate::store::TorrentStore;
|
||||||
use crate::api;
|
use crate::api;
|
||||||
|
|
||||||
@@ -11,17 +13,10 @@ pub fn AddTorrentDialog(
|
|||||||
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
let store = use_context::<TorrentStore>().expect("TorrentStore not provided");
|
||||||
let notifications = store.notifications;
|
let notifications = store.notifications;
|
||||||
|
|
||||||
let dialog_ref = NodeRef::<html::Dialog>::new();
|
|
||||||
let uri = signal(String::new());
|
let uri = signal(String::new());
|
||||||
let is_loading = signal(false);
|
let is_loading = signal(false);
|
||||||
let error_msg = signal(Option::<String>::None);
|
let error_msg = signal(Option::<String>::None);
|
||||||
|
|
||||||
Effect::new(move |_| {
|
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
|
||||||
let _ = dialog.show_modal();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let handle_submit = move |ev: web_sys::SubmitEvent| {
|
let handle_submit = move |ev: web_sys::SubmitEvent| {
|
||||||
ev.prevent_default();
|
ev.prevent_default();
|
||||||
let uri_val = uri.0.get();
|
let uri_val = uri.0.get();
|
||||||
@@ -44,9 +39,6 @@ pub fn AddTorrentDialog(
|
|||||||
shared::NotificationLevel::Success,
|
shared::NotificationLevel::Success,
|
||||||
"Torrent başarıyla eklendi"
|
"Torrent başarıyla eklendi"
|
||||||
);
|
);
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
|
||||||
dialog.close();
|
|
||||||
}
|
|
||||||
on_close.run(());
|
on_close.run(());
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
@@ -58,51 +50,76 @@ pub fn AddTorrentDialog(
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_cancel = move |_| {
|
let handle_backdrop = {
|
||||||
if let Some(dialog) = dialog_ref.get() {
|
let on_close = on_close.clone();
|
||||||
dialog.close();
|
move |e: web_sys::MouseEvent| {
|
||||||
|
e.stop_propagation();
|
||||||
|
on_close.run(());
|
||||||
}
|
}
|
||||||
on_close.run(());
|
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<dialog node_ref=dialog_ref class="modal modal-bottom sm:modal-middle">
|
// Backdrop overlay
|
||||||
<div class="modal-box">
|
<div
|
||||||
<h3 class="font-bold text-lg">"Add Torrent"</h3>
|
class="fixed inset-0 z-50 bg-background/80 backdrop-blur-sm"
|
||||||
<p class="py-4 text-sm opacity-70">"Enter a Magnet link or a .torrent file URL."</p>
|
on:click=handle_backdrop
|
||||||
|
/>
|
||||||
<form on:submit=handle_submit>
|
// Dialog panel
|
||||||
<div class="form-control w-full">
|
<div class="fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-card p-6 shadow-lg rounded-lg sm:max-w-[425px]">
|
||||||
<input
|
// Header
|
||||||
type="text"
|
<div class="flex flex-col space-y-1.5 text-center sm:text-left">
|
||||||
placeholder="magnet:?xt=urn:btih:..."
|
<h2 class="text-lg font-semibold leading-none tracking-tight">"Add Torrent"</h2>
|
||||||
class="input input-bordered w-full"
|
<p class="text-sm text-muted-foreground">"Enter a Magnet link or a .torrent file URL."</p>
|
||||||
prop:value=move || uri.0.get()
|
|
||||||
on:input=move |ev| uri.1.set(event_target_value(&ev))
|
|
||||||
disabled=move || is_loading.0.get()
|
|
||||||
autofocus
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="modal-action">
|
|
||||||
<button type="button" class="btn btn-ghost" on:click=handle_cancel>"Cancel"</button>
|
|
||||||
<button type="submit" class="btn btn-primary" disabled=move || is_loading.0.get()>
|
|
||||||
{move || if is_loading.0.get() {
|
|
||||||
leptos::either::Either::Left(view! { <span class="loading loading-spinner"></span> "Adding..." })
|
|
||||||
} else {
|
|
||||||
leptos::either::Either::Right(view! { "Add" })
|
|
||||||
}}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
|
|
||||||
{move || error_msg.0.get().map(|msg| view! {
|
|
||||||
<div class="text-error text-sm mt-2">{msg}</div>
|
|
||||||
})}
|
|
||||||
</div>
|
</div>
|
||||||
<form method="dialog" class="modal-backdrop">
|
|
||||||
<button on:click=handle_cancel>"close"</button>
|
<form on:submit=handle_submit class="space-y-4">
|
||||||
|
<Input
|
||||||
|
input_type="text"
|
||||||
|
placeholder="magnet:?xt=urn:btih:..."
|
||||||
|
value=MaybeProp::derive(move || Some(uri.0.get()))
|
||||||
|
on_change=Callback::new(move |val: String| uri.1.set(val))
|
||||||
|
disabled=Signal::derive(move || is_loading.0.get())
|
||||||
|
/>
|
||||||
|
|
||||||
|
{move || error_msg.0.get().map(|msg| view! {
|
||||||
|
<Alert variant=AlertVariant::Destructive>
|
||||||
|
<AlertDescription>{msg}</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
})}
|
||||||
|
|
||||||
|
<div class="flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2">
|
||||||
|
<Button
|
||||||
|
variant=ButtonVariant::Ghost
|
||||||
|
on_click=Callback::new(move |()| {
|
||||||
|
on_close.run(());
|
||||||
|
})
|
||||||
|
>
|
||||||
|
"Cancel"
|
||||||
|
</Button>
|
||||||
|
<Button disabled=Signal::derive(move || is_loading.0.get())>
|
||||||
|
{move || if is_loading.0.get() {
|
||||||
|
leptos::either::Either::Left(view! {
|
||||||
|
<span class="animate-spin mr-2 h-4 w-4 border-2 border-current border-t-transparent rounded-full"></span>
|
||||||
|
"Adding..."
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
leptos::either::Either::Right(view! { "Add" })
|
||||||
|
}}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</dialog>
|
|
||||||
|
// Close button (X)
|
||||||
|
<button
|
||||||
|
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none"
|
||||||
|
on:click=move |_| on_close.run(())
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="h-4 w-4">
|
||||||
|
<path d="M18 6 6 18"></path>
|
||||||
|
<path d="m6 6 12 12"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">"Close"</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
156
frontend/src/components/torrent/detail.rs
Normal file
156
frontend/src/components/torrent/detail.rs
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos_shadcn_tabs::{Tabs, TabsList, TabsTrigger, TabsContent};
|
||||||
|
|
||||||
|
fn format_bytes(bytes: i64) -> String {
|
||||||
|
const UNITS: [&str; 6] = ["B", "KB", "MB", "GB", "TB", "PB"];
|
||||||
|
if bytes < 1024 { return format!("{} B", bytes); }
|
||||||
|
let i = (bytes as f64).log2().div_euclid(10.0) as usize;
|
||||||
|
format!("{:.1} {}", (bytes as f64) / 1024_f64.powi(i as i32), UNITS[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_speed(bytes_per_sec: i64) -> String {
|
||||||
|
if bytes_per_sec == 0 { return "0 B/s".to_string(); }
|
||||||
|
format!("{}/s", format_bytes(bytes_per_sec))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_date(timestamp: i64) -> String {
|
||||||
|
if timestamp <= 0 { return "N/A".to_string(); }
|
||||||
|
let dt = chrono::DateTime::from_timestamp(timestamp, 0);
|
||||||
|
match dt { Some(dt) => dt.format("%d/%m/%Y %H:%M").to_string(), None => "N/A".to_string() }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn format_duration(seconds: i64) -> String {
|
||||||
|
if seconds <= 0 { return "∞".to_string(); }
|
||||||
|
let days = seconds / 86400;
|
||||||
|
let hours = (seconds % 86400) / 3600;
|
||||||
|
let minutes = (seconds % 3600) / 60;
|
||||||
|
let secs = seconds % 60;
|
||||||
|
if days > 0 { format!("{}d {}h", days, hours) }
|
||||||
|
else if hours > 0 { format!("{}h {}m", hours, minutes) }
|
||||||
|
else if minutes > 0 { format!("{}m {}s", minutes, secs) }
|
||||||
|
else { format!("{}s", secs) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TorrentDetail() -> impl IntoView {
|
||||||
|
let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
|
||||||
|
|
||||||
|
let torrent = Memo::new(move |_| {
|
||||||
|
let hash = store.selected_torrent.get()?;
|
||||||
|
store.torrents.with(|map| map.get(&hash).cloned())
|
||||||
|
});
|
||||||
|
|
||||||
|
let close = move |_| {
|
||||||
|
store.selected_torrent.set(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Show when=move || torrent.get().is_some()>
|
||||||
|
{move || {
|
||||||
|
let t = torrent.get().unwrap();
|
||||||
|
let name = t.name.clone();
|
||||||
|
let status_color = match t.status {
|
||||||
|
shared::TorrentStatus::Seeding => "text-green-500",
|
||||||
|
shared::TorrentStatus::Downloading => "text-blue-500",
|
||||||
|
shared::TorrentStatus::Paused => "text-yellow-500",
|
||||||
|
shared::TorrentStatus::Error => "text-red-500",
|
||||||
|
_ => "text-muted-foreground",
|
||||||
|
};
|
||||||
|
let status_text = format!("{:?}", t.status);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="border-t border-border bg-card flex flex-col" style="height: 280px; min-height: 200px;">
|
||||||
|
// Header
|
||||||
|
<div class="flex items-center justify-between px-4 py-2 border-b border-border bg-muted/30">
|
||||||
|
<div class="flex items-center gap-3 min-w-0 flex-1">
|
||||||
|
<h3 class="text-sm font-semibold truncate">{name}</h3>
|
||||||
|
<span class={format!("text-xs font-medium {}", status_color)}>{status_text}</span>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
class="inline-flex items-center justify-center rounded-md text-sm font-medium hover:bg-accent hover:text-accent-foreground h-7 w-7 text-muted-foreground shrink-0"
|
||||||
|
on:click=close
|
||||||
|
title="Close"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-4 h-4">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
// Tabs
|
||||||
|
<Tabs default_value="general" class="flex-1 flex flex-col overflow-hidden">
|
||||||
|
<div class="px-4 pt-2">
|
||||||
|
<TabsList class="w-full">
|
||||||
|
<TabsTrigger value="general">"General"</TabsTrigger>
|
||||||
|
<TabsTrigger value="transfer">"Transfer"</TabsTrigger>
|
||||||
|
<TabsTrigger value="files">"Files"</TabsTrigger>
|
||||||
|
<TabsTrigger value="peers">"Peers"</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TabsContent value="general" class="flex-1 overflow-y-auto px-4 pb-3">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 text-sm">
|
||||||
|
<DetailItem label="Size" value=format_bytes(t.size) />
|
||||||
|
<DetailItem label="Downloaded" value=format_bytes(t.completed) />
|
||||||
|
<DetailItem label="Progress" value=format!("{:.1}%", t.percent_complete) />
|
||||||
|
<DetailItem label="Added" value=format_date(t.added_date) />
|
||||||
|
<DetailItem label="Hash" value={
|
||||||
|
let hash = store.selected_torrent.get().unwrap_or_default();
|
||||||
|
format!("{}…", &hash[..std::cmp::min(16, hash.len())])
|
||||||
|
} />
|
||||||
|
<DetailItem label="Label" value=t.label.clone().unwrap_or_else(|| "—".to_string()) />
|
||||||
|
<DetailItem label="Error" value={
|
||||||
|
if t.error_message.is_empty() { "None".to_string() } else { t.error_message.clone() }
|
||||||
|
} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="transfer" class="flex-1 overflow-y-auto px-4 pb-3">
|
||||||
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-x-6 gap-y-2 text-sm">
|
||||||
|
<DetailItem label="Download Speed" value=format_speed(t.down_rate) />
|
||||||
|
<DetailItem label="Upload Speed" value=format_speed(t.up_rate) />
|
||||||
|
<DetailItem label="ETA" value=format_duration(t.eta) />
|
||||||
|
<DetailItem label="Downloaded" value=format_bytes(t.completed) />
|
||||||
|
<DetailItem label="Total Size" value=format_bytes(t.size) />
|
||||||
|
<DetailItem label="Remaining" value=format_bytes(t.size - t.completed) />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="files" class="flex-1 overflow-y-auto px-4 pb-3">
|
||||||
|
<div class="text-sm text-muted-foreground flex items-center gap-2 py-4">
|
||||||
|
<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="M2.25 12.75V12A2.25 2.25 0 014.5 9.75h15A2.25 2.25 0 0121.75 12v.75m-8.69-6.44l-2.12-2.12a1.5 1.5 0 00-1.061-.44H4.5A2.25 2.25 0 002.25 6v12a2.25 2.25 0 002.25 2.25h15A2.25 2.25 0 0021.75 18V9a2.25 2.25 0 00-2.25-2.25h-5.379a1.5 1.5 0 01-1.06-.44z" />
|
||||||
|
</svg>
|
||||||
|
"File list will be available when file API is connected."
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="peers" class="flex-1 overflow-y-auto px-4 pb-3">
|
||||||
|
<div class="text-sm text-muted-foreground flex items-center gap-2 py-4">
|
||||||
|
<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 18.72a9.094 9.094 0 003.741-.479 3 3 0 00-4.682-2.72m.94 3.198l.001.031c0 .225-.012.447-.037.666A11.944 11.944 0 0112 21c-2.17 0-4.207-.576-5.963-1.584A6.062 6.062 0 016 18.719m12 0a5.971 5.971 0 00-.941-3.197m0 0A5.995 5.995 0 0012 12.75a5.995 5.995 0 00-5.058 2.772m0 0a3 3 0 00-4.681 2.72 8.986 8.986 0 003.74.477m.94-3.197a5.971 5.971 0 00-.94 3.197M15 6.75a3 3 0 11-6 0 3 3 0 016 0zm6 3a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0zm-13.5 0a2.25 2.25 0 11-4.5 0 2.25 2.25 0 014.5 0z" />
|
||||||
|
</svg>
|
||||||
|
"Peer list will be available when peer API is connected."
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
</Show>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn DetailItem(
|
||||||
|
#[prop(into)] label: String,
|
||||||
|
#[prop(into)] value: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let title = value.clone();
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col gap-0.5 py-1">
|
||||||
|
<span class="text-[10px] uppercase tracking-wider text-muted-foreground font-medium">{label}</span>
|
||||||
|
<span class="text-foreground font-mono text-xs truncate" title=title>{value}</span>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,2 +1,3 @@
|
|||||||
pub mod table;
|
pub mod table;
|
||||||
pub mod add_torrent;
|
pub mod add_torrent;
|
||||||
|
pub mod detail;
|
||||||
|
|||||||
@@ -210,6 +210,8 @@ fn TorrentRow(
|
|||||||
let h = hash.clone();
|
let h = hash.clone();
|
||||||
let torrent = Memo::new(move |_| store.torrents.with(|map| map.get(&h).cloned()));
|
let torrent = Memo::new(move |_| store.torrents.with(|map| map.get(&h).cloned()));
|
||||||
|
|
||||||
|
let stored_hash = StoredValue::new(hash.clone());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || torrent.get().is_some() fallback=|| ()>
|
<Show when=move || torrent.get().is_some() fallback=|| ()>
|
||||||
{
|
{
|
||||||
@@ -219,7 +221,18 @@ fn TorrentRow(
|
|||||||
let status_color = match t.status { shared::TorrentStatus::Seeding => "text-green-500", shared::TorrentStatus::Downloading => "text-blue-500", shared::TorrentStatus::Paused => "text-yellow-500", shared::TorrentStatus::Error => "text-red-500", _ => "text-muted-foreground" };
|
let status_color = match t.status { shared::TorrentStatus::Seeding => "text-green-500", shared::TorrentStatus::Downloading => "text-blue-500", shared::TorrentStatus::Paused => "text-yellow-500", shared::TorrentStatus::Error => "text-red-500", _ => "text-muted-foreground" };
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex items-center text-sm hover:bg-muted/50 border-b border-border h-[48px] px-2 select-none cursor-pointer transition-colors w-full">
|
<div
|
||||||
|
class=move || {
|
||||||
|
let selected = store.selected_torrent.get();
|
||||||
|
let is_selected = selected.as_deref() == Some(stored_hash.get_value().as_str());
|
||||||
|
if is_selected {
|
||||||
|
"flex items-center text-sm bg-primary/10 border-b border-border h-[48px] px-2 select-none cursor-pointer transition-colors w-full"
|
||||||
|
} else {
|
||||||
|
"flex items-center text-sm hover:bg-muted/50 border-b border-border h-[48px] px-2 select-none cursor-pointer transition-colors w-full"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
|
||||||
|
>
|
||||||
<div class="flex-1 min-w-0 px-2 font-medium truncate" title=t_name.clone()>{t_name.clone()}</div>
|
<div class="flex-1 min-w-0 px-2 font-medium truncate" title=t_name.clone()>{t_name.clone()}</div>
|
||||||
<div class="w-24 px-2 font-mono text-xs text-muted-foreground">{format_bytes(t.size)}</div>
|
<div class="w-24 px-2 font-mono text-xs text-muted-foreground">{format_bytes(t.size)}</div>
|
||||||
<div class="w-48 px-2">
|
<div class="w-48 px-2">
|
||||||
@@ -251,6 +264,8 @@ fn TorrentCard(
|
|||||||
let h = hash.clone();
|
let h = hash.clone();
|
||||||
let torrent = Memo::new(move |_| store.torrents.with(|map| map.get(&h).cloned()));
|
let torrent = Memo::new(move |_| store.torrents.with(|map| map.get(&h).cloned()));
|
||||||
|
|
||||||
|
let stored_hash = StoredValue::new(hash.clone());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Show when=move || torrent.get().is_some() fallback=|| ()>
|
<Show when=move || torrent.get().is_some() fallback=|| ()>
|
||||||
{
|
{
|
||||||
@@ -260,6 +275,18 @@ fn TorrentCard(
|
|||||||
let status_badge_class = match t.status { shared::TorrentStatus::Seeding => "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 border-green-200 dark:border-green-800", shared::TorrentStatus::Downloading => "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 border-blue-200 dark:border-blue-800", shared::TorrentStatus::Paused => "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800", shared::TorrentStatus::Error => "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 border-red-200 dark:border-red-800", _ => "bg-muted text-muted-foreground" };
|
let status_badge_class = match t.status { shared::TorrentStatus::Seeding => "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400 border-green-200 dark:border-green-800", shared::TorrentStatus::Downloading => "bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 border-blue-200 dark:border-blue-800", shared::TorrentStatus::Paused => "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800", shared::TorrentStatus::Error => "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400 border-red-200 dark:border-red-800", _ => "bg-muted text-muted-foreground" };
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
|
<div
|
||||||
|
class=move || {
|
||||||
|
let selected = store.selected_torrent.get();
|
||||||
|
let is_selected = selected.as_deref() == Some(stored_hash.get_value().as_str());
|
||||||
|
if is_selected {
|
||||||
|
"ring-2 ring-primary rounded-lg transition-all"
|
||||||
|
} else {
|
||||||
|
"transition-all"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
|
||||||
|
>
|
||||||
<Card class="h-full select-none cursor-pointer hover:border-primary transition-colors">
|
<Card class="h-full select-none cursor-pointer hover:border-primary transition-colors">
|
||||||
<CardHeader class="p-3 pb-0">
|
<CardHeader class="p-3 pb-0">
|
||||||
<div class="flex justify-between items-start gap-2">
|
<div class="flex justify-between items-start gap-2">
|
||||||
@@ -285,6 +312,7 @@ fn TorrentCard(
|
|||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ pub struct TorrentStore {
|
|||||||
pub global_stats: RwSignal<GlobalStats>,
|
pub global_stats: RwSignal<GlobalStats>,
|
||||||
pub notifications: RwSignal<Vec<NotificationItem>>,
|
pub notifications: RwSignal<Vec<NotificationItem>>,
|
||||||
pub user: RwSignal<Option<String>>,
|
pub user: RwSignal<Option<String>>,
|
||||||
|
pub selected_torrent: RwSignal<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn provide_torrent_store() {
|
pub fn provide_torrent_store() {
|
||||||
@@ -78,10 +79,11 @@ pub fn provide_torrent_store() {
|
|||||||
let global_stats = RwSignal::new(GlobalStats::default());
|
let global_stats = RwSignal::new(GlobalStats::default());
|
||||||
let notifications = RwSignal::new(Vec::<NotificationItem>::new());
|
let notifications = RwSignal::new(Vec::<NotificationItem>::new());
|
||||||
let user = RwSignal::new(Option::<String>::None);
|
let user = RwSignal::new(Option::<String>::None);
|
||||||
|
let selected_torrent = RwSignal::new(Option::<String>::None);
|
||||||
|
|
||||||
let show_browser_notification = crate::utils::notification::use_app_notification();
|
let show_browser_notification = crate::utils::notification::use_app_notification();
|
||||||
|
|
||||||
let store = TorrentStore { torrents, filter, search_query, global_stats, notifications, user };
|
let store = TorrentStore { torrents, filter, search_query, global_stats, notifications, user, selected_torrent };
|
||||||
provide_context(store);
|
provide_context(store);
|
||||||
|
|
||||||
let notifications_for_sse = notifications;
|
let notifications_for_sse = notifications;
|
||||||
|
|||||||
@@ -1,6 +1,19 @@
|
|||||||
|
const path = require("path");
|
||||||
|
const os = require("os");
|
||||||
|
|
||||||
|
// Cargo registry'deki leptos-shadcn crate'lerini Tailwind'e taratmak için
|
||||||
|
const cargoRegistry = path.join(
|
||||||
|
os.homedir(),
|
||||||
|
".cargo/registry/src/*/leptos-shadcn-*/src/**/*.rs"
|
||||||
|
);
|
||||||
|
|
||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
module.exports = {
|
module.exports = {
|
||||||
content: ["./index.html", "./src/**/*.{rs,html}"],
|
content: [
|
||||||
|
"./index.html",
|
||||||
|
"./src/**/*.{rs,html}",
|
||||||
|
cargoRegistry,
|
||||||
|
],
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
|||||||
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