Compare commits

...

3 Commits

Author SHA1 Message Date
spinline
71456ff4d1 fix: mobil detay paneli tam ekran genişliğine alındı
All checks were successful
Build MIPS Binary / build (push) Successful in 2m3s
2026-02-21 22:03:09 +03:00
spinline
1a3099d926 fix: mobil detay paneli sağdan açılacak şekilde güncellendi
All checks were successful
Build MIPS Binary / build (push) Successful in 2m0s
2026-02-21 21:58:50 +03:00
spinline
4ef4ee8d45 feat: sheet yerine sayfaya sabit inline detay paneli eklendi
All checks were successful
Build MIPS Binary / build (push) Successful in 2m3s
2026-02-21 21:48:18 +03:00
2 changed files with 111 additions and 88 deletions

View File

@@ -1,76 +1,115 @@
use leptos::prelude::*; use leptos::prelude::*;
use crate::components::ui::sheet::*;
use crate::components::ui::tabs::*; use crate::components::ui::tabs::*;
use crate::components::ui::skeleton::*; use crate::components::ui::skeleton::*;
use shared::Torrent;
#[component] #[component]
pub fn TorrentDetailsSheet() -> impl IntoView { pub fn TorrentDetailsPanel() -> impl IntoView {
let store = use_context::<crate::store::TorrentStore>().expect("store not provided"); let store = use_context::<crate::store::TorrentStore>().expect("store not provided");
// Setup an effect to open the sheet when a torrent is selected
Effect::new(move |_| {
if store.selected_torrent.get().is_some() {
if let Some(trigger) = document().get_element_by_id("torrent-details-trigger") {
use wasm_bindgen::JsCast;
let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el| el.click());
}
}
});
let selected_torrent = Memo::new(move |_| { let selected_torrent = Memo::new(move |_| {
let hash = store.selected_torrent.get()?; let hash = store.selected_torrent.get()?;
store.torrents.with(|map| map.get(&hash).cloned()) store.torrents.with(|map| map.get(&hash).cloned())
}); });
let is_open = Signal::derive(move || store.selected_torrent.get().is_some());
view! { view! {
<Sheet> // Mobil overlay backdrop
<SheetTrigger attr:id="torrent-details-trigger" class="hidden">""</SheetTrigger> <div
<SheetContent class=move || if is_open.get() {
direction=SheetDirection::Bottom "fixed inset-0 bg-black/40 z-30 md:hidden backdrop-blur-sm transition-opacity duration-300 opacity-100"
class="h-[80vh] sm:h-[60vh] rounded-t-xl sm:rounded-t-2xl p-0 flex flex-col gap-0 border-t border-border shadow-2xl" } else {
hide_close_button=true "fixed inset-0 bg-black/0 z-30 md:hidden pointer-events-none transition-opacity duration-300 opacity-0"
> }
<div class="px-6 py-4 border-b flex items-center justify-between sticky top-0 bg-card z-10"> on:click=move |_| store.selected_torrent.set(None)
<div class="flex flex-col gap-1 min-w-0"> />
<Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-6 w-48" /> }>
<h2 class="font-bold text-lg truncate"> // Panel — masaüstünde sağ kolonda sabit, mobilde sağdan açılan overlay
<div class=move || {
if is_open.get() {
// Açık: masaüstünde görünür, mobilde sağdan gelir
"w-full md:w-[380px] md:min-w-[380px] shrink-0 \
flex flex-col border-l border-border bg-card \
fixed top-0 right-0 bottom-0 z-40 \
translate-x-0 \
md:static md:z-auto md:translate-x-0 \
transition-transform duration-300 ease-out shadow-2xl md:shadow-none"
} else {
// Kapalı: masaüstünde gizli, mobilde sağa kayar
"w-full md:w-0 shrink-0 overflow-hidden border-none \
fixed top-0 right-0 bottom-0 z-40 \
translate-x-full \
md:static md:z-auto md:translate-x-0 \
transition-transform duration-300 ease-in pointer-events-none"
}
}>
// İpucu: panel kapalıyken içeriği render etme
<Show when=move || is_open.get()>
// Başlık
<div class="px-4 py-3 border-b flex items-center justify-between shrink-0 bg-card">
<div class="flex flex-col gap-0.5 min-w-0 flex-1">
<Show
when=move || selected_torrent.get().is_some()
fallback=move || view! { <Skeleton class="h-5 w-40" /> }
>
<h2 class="font-bold text-sm truncate leading-tight">
{move || selected_torrent.get().map(|t| t.name).unwrap_or_default()} {move || selected_torrent.get().map(|t| t.name).unwrap_or_default()}
</h2> </h2>
</Show> </Show>
<Show when=move || selected_torrent.get().is_some() fallback=move || view! { <Skeleton class="h-4 w-24" /> }> <Show
<p class="text-xs text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-2"> when=move || selected_torrent.get().is_some()
fallback=move || view! { <Skeleton class="h-3 w-20 mt-1" /> }
>
<p class="text-[10px] text-muted-foreground uppercase tracking-widest font-semibold flex items-center gap-1.5">
{move || selected_torrent.get().map(|t| format!("{:?}", t.status)).unwrap_or_default()} {move || selected_torrent.get().map(|t| format!("{:?}", t.status)).unwrap_or_default()}
<span class="bg-primary/20 text-primary px-1.5 py-0.5 rounded text-[10px] lowercase">{move || selected_torrent.get().map(|t| format!("{:.1}%", t.percent_complete)).unwrap_or_default()}</span> <span class="bg-primary/20 text-primary px-1 py-0.5 rounded text-[9px] lowercase">
{move || selected_torrent.get().map(|t| format!("{:.1}%", t.percent_complete)).unwrap_or_default()}
</span>
</p> </p>
</Show> </Show>
</div> </div>
// Custom close button that also resets store.selected_torrent // Kapat butonu
<SheetClose class="rounded-full p-2 hover:bg-muted transition-colors border-none shadow-none cursor-pointer"> <button
<icons::X class="size-5 opacity-70" on:click=move |_| store.selected_torrent.set(None) /> class="rounded-full p-1.5 hover:bg-muted transition-colors text-muted-foreground hover:text-foreground shrink-0 ml-2"
</SheetClose> on:click=move |_| store.selected_torrent.set(None)
>
<icons::X class="size-4" />
</button>
</div> </div>
<div class="flex-1 overflow-hidden flex flex-col pt-4 px-6 pb-0"> // Sekmeler + içerik
<div class="flex-1 overflow-hidden flex flex-col min-h-0">
<Tabs default_value="general" class="flex-1 h-full min-h-0 flex flex-col"> <Tabs default_value="general" class="flex-1 h-full min-h-0 flex flex-col">
<TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0 shrink-0"> <TabsList class="w-full justify-start rounded-none border-b bg-transparent p-0 shrink-0 px-2">
<TabsTrigger value="general" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none"> <TabsTrigger
value="general"
class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none text-xs h-9"
>
"Genel" "Genel"
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="files" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none"> <TabsTrigger
value="files"
class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none text-xs h-9"
>
"Dosyalar" "Dosyalar"
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="trackers" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none"> <TabsTrigger
value="trackers"
class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none text-xs h-9"
>
"İzleyiciler" "İzleyiciler"
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="peers" class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none"> <TabsTrigger
value="peers"
class="data-[state=active]:bg-transparent data-[state=active]:shadow-none data-[state=active]:border-b-2 data-[state=active]:border-primary rounded-none text-xs h-9"
>
"Eşler" "Eşler"
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
<crate::components::ui::scroll_area::ScrollArea class="flex-1 min-h-0 mt-4 pb-12 pr-4"> <crate::components::ui::scroll_area::ScrollArea class="flex-1 min-h-0">
<TabsContent value="general" class="space-y-6 animate-in fade-in slide-in-from-bottom-2 duration-300"> <TabsContent value="general" class="p-4 space-y-5 animate-in fade-in duration-200">
<crate::components::ui::shimmer::Shimmer <crate::components::ui::shimmer::Shimmer
loading=Signal::derive(move || selected_torrent.get().is_none()) loading=Signal::derive(move || selected_torrent.get().is_none())
shimmer_color="rgba(0,0,0,0.06)" shimmer_color="rgba(0,0,0,0.06)"
background_color="rgba(0,0,0,0.04)" background_color="rgba(0,0,0,0.04)"
@@ -97,49 +136,31 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
}); });
view! { view! {
<div class="flex flex-col gap-6"> <div class="flex flex-col gap-5">
// Aktarım // Aktarım
<div> <div>
<h3 class="text-sm font-bold border-b pb-2 mb-4">"Aktarım"</h3> <h3 class="text-[10px] font-bold border-b pb-1.5 mb-3 uppercase tracking-widest text-muted-foreground">"Aktarım"</h3>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4"> <div class="grid grid-cols-2 gap-3">
<InfoItem label="Geçen Süre" value="N/A".to_string() />
<InfoItem label="Kalan" value=format_duration(t.eta) /> <InfoItem label="Kalan" value=format_duration(t.eta) />
<InfoItem label="Paylaşım Oranı" value=format!("{:.3}", t.ratio) /> <InfoItem label="Paylaşım Oranı" value=format!("{:.3}", t.ratio) />
<div class="hidden md:block"></div>
<InfoItem label="İndirilen" value=format_bytes(t.completed) /> <InfoItem label="İndirilen" value=format_bytes(t.completed) />
<InfoItem label="İndirme Hızı" value=format_speed(t.down_rate) class="text-blue-500" /> <InfoItem label="İndirme Hızı" value=format_speed(t.down_rate) class="text-blue-500" />
<InfoItem label="Boşa Giden" value=format_bytes(t.wasted) />
<div class="hidden md:block"></div>
<InfoItem label="Gönderilen" value=format_bytes(t.uploaded) /> <InfoItem label="Gönderilen" value=format_bytes(t.uploaded) />
<InfoItem label="Gönderme Hızı" value=format_speed(t.up_rate) class="text-green-500" /> <InfoItem label="Gönderme Hızı" value=format_speed(t.up_rate) class="text-green-500" />
<div class="hidden md:block"></div> <InfoItem label="Boşa Giden" value=format_bytes(t.wasted) />
<div class="hidden md:block"></div>
<InfoItem label="Ortaklar" value="0 / 0 bağlı".to_string() />
<InfoItem label="Eşler" value="0 / 0 bağlı".to_string() />
</div>
</div>
// İzleyici
<div>
<h3 class="text-sm font-bold border-b pb-2 mb-4">"İzleyici"</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<InfoItem label="İzleyici URL'si" value="İzleyiciler sekmesine bakınız".to_string() class="col-span-1 md:col-span-2 break-all text-xs" />
<InfoItem label="İzleyici Durumu" value="N/A".to_string() />
</div> </div>
</div> </div>
// Genel // Genel
<div> <div>
<h3 class="text-sm font-bold border-b pb-2 mb-4">"Genel"</h3> <h3 class="text-[10px] font-bold border-b pb-1.5 mb-3 uppercase tracking-widest text-muted-foreground">"Genel"</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4"> <div class="flex flex-col gap-3">
<InfoItem label="Kaydedilen Yer" value=t.save_path class="col-span-1 md:col-span-2 break-all font-mono text-xs" /> <InfoItem label="Kaydedilen Yer" value=t.save_path class="break-all font-mono text-xs" />
<InfoItem label="Boş Disk Alanı" value=format_bytes(t.free_disk_space) /> <div class="grid grid-cols-2 gap-3">
<InfoItem label="Oluşturulma Tarihi" value=format_date(t.added_date) /> <InfoItem label="Boş Disk Alanı" value=format_bytes(t.free_disk_space) />
<InfoItem label="Hash" value=t.hash class="col-span-1 md:col-span-2 break-all font-mono text-xs" /> <InfoItem label="Oluşturulma Tarihi" value=format_date(t.added_date) />
<InfoItem label="Yorum" value="Yok".to_string() class="col-span-1 md:col-span-2 break-words text-xs" /> </div>
<InfoItem label="Hash" value=t.hash class="break-all font-mono text-[10px]" />
</div> </div>
</div> </div>
</div> </div>
@@ -156,8 +177,8 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
</div> </div>
}), }),
None => leptos::either::Either::Right(view! { None => leptos::either::Either::Right(view! {
<div class="flex flex-col items-center justify-center h-48 opacity-60"> <div class="flex flex-col items-center justify-center h-48 opacity-60 gap-2">
<icons::File class="size-12 mb-3 text-muted-foreground" /> <icons::File class="size-10 text-muted-foreground" />
<p class="text-sm font-medium">"Dosya yükleniyor..."</p> <p class="text-sm font-medium">"Dosya yükleniyor..."</p>
</div> </div>
}), }),
@@ -172,8 +193,8 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
</div> </div>
}), }),
None => leptos::either::Either::Right(view! { None => leptos::either::Either::Right(view! {
<div class="flex flex-col items-center justify-center h-48 opacity-60"> <div class="flex flex-col items-center justify-center h-48 opacity-60 gap-2">
<icons::Settings2 class="size-12 mb-3 text-muted-foreground" /> <icons::Settings2 class="size-10 text-muted-foreground" />
<p class="text-sm font-medium">"İzleyici yükleniyor..."</p> <p class="text-sm font-medium">"İzleyici yükleniyor..."</p>
</div> </div>
}), }),
@@ -181,29 +202,29 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
</TabsContent> </TabsContent>
<TabsContent value="peers" class="h-full"> <TabsContent value="peers" class="h-full">
<div class="flex flex-col items-center justify-center h-48 opacity-60"> <div class="flex flex-col items-center justify-center h-48 opacity-60 gap-2">
<icons::Users class="size-12 mb-3 text-muted-foreground" /> <icons::Users class="size-10 text-muted-foreground" />
<p class="text-sm font-medium">"Eş listesi yakında eklenecek"</p> <p class="text-sm font-medium">"Eş listesi yakında eklenecek"</p>
</div> </div>
</TabsContent> </TabsContent>
</crate::components::ui::scroll_area::ScrollArea> </crate::components::ui::scroll_area::ScrollArea>
</Tabs> </Tabs>
</div> </div>
</SheetContent> </Show>
</Sheet> </div>
} }
} }
#[component] #[component]
fn InfoItem( fn InfoItem(
label: &'static str, label: &'static str,
value: String, value: String,
#[prop(optional)] class: &'static str #[prop(optional)] class: &'static str
) -> impl IntoView { ) -> impl IntoView {
view! { view! {
<div class=tailwind_fuse::tw_merge!("flex flex-col gap-1", class)> <div class=tailwind_fuse::tw_merge!("flex flex-col gap-0.5", class)>
<span class="text-xs font-semibold text-muted-foreground uppercase opacity-80">{label}</span> <span class="text-[9px] font-semibold text-muted-foreground uppercase tracking-wider opacity-70">{label}</span>
<span class="text-sm font-medium">{value}</span> <span class="text-xs font-medium leading-tight">{value}</span>
</div> </div>
} }
} }

View File

@@ -218,7 +218,9 @@ pub fn TorrentTable() -> impl IntoView {
}); });
view! { view! {
<div class="h-full bg-background relative flex flex-col overflow-hidden px-4 py-4 gap-4"> <div class="h-full bg-background flex flex-row overflow-hidden">
// Sol: liste alanı
<div class="flex-1 min-w-0 flex flex-col overflow-hidden px-4 py-4 gap-4">
// --- TOPBAR --- // --- TOPBAR ---
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<div class="flex items-center gap-2 flex-1 max-w-md"> <div class="flex items-center gap-2 flex-1 max-w-md">
@@ -249,7 +251,6 @@ pub fn TorrentTable() -> impl IntoView {
<div class="my-1 h-px bg-border" /> <div class="my-1 h-px bg-border" />
// Trigger the hidden AlertDialog from this menu item
<DropdownMenuItem class="text-destructive focus:bg-destructive/10 cursor-pointer" on:click=move |_| { <DropdownMenuItem class="text-destructive focus:bg-destructive/10 cursor-pointer" on:click=move |_| {
if let Some(trigger) = document().get_element_by_id("bulk-delete-trigger") { if let Some(trigger) = document().get_element_by_id("bulk-delete-trigger") {
let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el: web_sys::HtmlElement| el.click()); let _ = trigger.dyn_into::<web_sys::HtmlElement>().map(|el: web_sys::HtmlElement| el.click());
@@ -261,7 +262,6 @@ pub fn TorrentTable() -> impl IntoView {
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
// Hidden AlertDialog moved outside the DropdownMenuContent to ensure proper centering
<AlertDialog> <AlertDialog>
<AlertDialogTrigger attr:id="bulk-delete-trigger" class="hidden">""</AlertDialogTrigger> <AlertDialogTrigger attr:id="bulk-delete-trigger" class="hidden">""</AlertDialogTrigger>
<AlertDialogContent class="sm:max-w-[425px]"> <AlertDialogContent class="sm:max-w-[425px]">
@@ -553,9 +553,11 @@ pub fn TorrentTable() -> impl IntoView {
</div> </div>
<div class="opacity-50">"VibeTorrent v3"</div> <div class="opacity-50">"VibeTorrent v3"</div>
</div> </div>
<crate::components::torrent::details::TorrentDetailsSheet />
</div> </div>
// Sağ: sabit detay paneli
<crate::components::torrent::details::TorrentDetailsPanel />
</div>
}.into_any() }.into_any()
} }