Compare commits
5 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
daa24dd7ec | ||
|
|
45271b5060 | ||
|
|
4d02bc655d | ||
|
|
3c2ba477f5 | ||
|
|
6106d1cd22 |
@@ -106,10 +106,19 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="files" class="h-full">
|
||||
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
||||
<icons::File class="size-12 mb-3 text-muted-foreground" />
|
||||
<p class="text-sm font-medium">"Dosya listesi yakında eklenecek"</p>
|
||||
</div>
|
||||
{move || match selected_torrent.get() {
|
||||
Some(t) => leptos::either::Either::Left(view! {
|
||||
<div class="h-full overflow-y-auto pr-2 pb-8">
|
||||
<crate::components::torrent::files::TorrentFilesTab hash=t.hash />
|
||||
</div>
|
||||
}),
|
||||
None => leptos::either::Either::Right(view! {
|
||||
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
||||
<icons::File class="size-12 mb-3 text-muted-foreground" />
|
||||
<p class="text-sm font-medium">"Dosya yükleniyor..."</p>
|
||||
</div>
|
||||
}),
|
||||
}}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="trackers" class="h-full">
|
||||
|
||||
225
frontend/src/components/torrent/files.rs
Normal file
225
frontend/src/components/torrent/files.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
use leptos::prelude::*;
|
||||
use crate::components::ui::table::*;
|
||||
use crate::components::ui::badge::*;
|
||||
use crate::components::ui::shimmer::*;
|
||||
use crate::components::ui::context_menu::*;
|
||||
use shared::TorrentFile;
|
||||
|
||||
#[component]
|
||||
pub fn TorrentFilesTab(hash: String) -> impl IntoView {
|
||||
let hash_clone = hash.clone();
|
||||
|
||||
// Fetch files resource
|
||||
let files_resource = Resource::new(
|
||||
move || hash_clone.clone(),
|
||||
|h| async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
|
||||
);
|
||||
|
||||
// Refresh action
|
||||
let refresh_files = Action::new(|h: &String| {
|
||||
let h = h.clone();
|
||||
async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
|
||||
});
|
||||
|
||||
let stored_hash = StoredValue::new(hash);
|
||||
|
||||
view! {
|
||||
<Suspense fallback=move || view! { <FilesFallback /> }>
|
||||
{move || {
|
||||
let files = match refresh_files.value().get() {
|
||||
Some(f) => f,
|
||||
None => files_resource.get().unwrap_or_default(),
|
||||
};
|
||||
|
||||
if files.is_empty() {
|
||||
return view! {
|
||||
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
||||
<icons::File class="size-12 mb-3 text-muted-foreground" />
|
||||
<p class="text-sm font-medium">"Bu torrent için dosya bulunamadı."</p>
|
||||
</div>
|
||||
}.into_any();
|
||||
}
|
||||
|
||||
let files_len = files.len();
|
||||
|
||||
view! {
|
||||
<div class="space-y-4">
|
||||
<TableWrapper class="bg-card/50">
|
||||
<Table>
|
||||
<TableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
|
||||
<TableRow class="hover:bg-transparent">
|
||||
<TableHead class="w-12 text-center text-xs">"#"</TableHead>
|
||||
<TableHead class="text-xs">"Dosya Adı"</TableHead>
|
||||
<TableHead class="w-24 text-right text-xs">"Boyut"</TableHead>
|
||||
<TableHead class="w-24 text-right text-xs">"Tamamlanan"</TableHead>
|
||||
<TableHead class="w-24 text-center text-xs">"Öncelik"</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
<For
|
||||
each=move || files.clone()
|
||||
key=|f| f.index
|
||||
children={move |f| {
|
||||
let p_hash = stored_hash.get_value();
|
||||
let r_files = refresh_files.clone();
|
||||
view! {
|
||||
<FileRow
|
||||
file=f
|
||||
hash=p_hash
|
||||
refresh_action=r_files
|
||||
/>
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</TableWrapper>
|
||||
|
||||
<div class="flex items-center justify-between text-xs text-muted-foreground px-1">
|
||||
<span>{format!("Toplam {} dosya", files_len)}</span>
|
||||
</div>
|
||||
</div>
|
||||
}.into_any()
|
||||
}}
|
||||
</Suspense>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<TorrentFile>>) -> impl IntoView {
|
||||
let f_idx = file.index;
|
||||
let context_id = format!("file-context-{}-{}", hash, f_idx);
|
||||
let path_clone = file.path.clone();
|
||||
|
||||
let set_priority = Action::new(|req: &(String, u32, u8)| {
|
||||
let (h, idx, p) = req.clone();
|
||||
async move {
|
||||
let res = shared::server_fns::torrent::set_file_priority(h, idx, p).await;
|
||||
if let Err(e) = &res {
|
||||
crate::store::show_toast(shared::NotificationLevel::Error, format!("Öncelik değiştirilemedi: {:?}", e));
|
||||
} else {
|
||||
crate::store::show_toast(shared::NotificationLevel::Success, "Dosya önceliği güncellendi.".to_string());
|
||||
}
|
||||
res
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<FileContextMenu
|
||||
torrent_hash=hash
|
||||
file_index=f_idx
|
||||
refresh_action=refresh_action
|
||||
set_priority=set_priority
|
||||
>
|
||||
<TableRow class="hover:bg-muted/50 transition-colors group">
|
||||
<TableCell class="text-center text-xs text-muted-foreground">{file.index}</TableCell>
|
||||
<TableCell class="font-medium text-xs break-all max-w-[200px] md:max-w-md" attr:title=move || path_clone.clone()>
|
||||
{file.path.clone()}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-xs text-muted-foreground whitespace-nowrap">
|
||||
{format_bytes(file.size)}
|
||||
</TableCell>
|
||||
<TableCell class="text-right text-xs whitespace-nowrap">
|
||||
<span class="text-primary font-medium">{format_bytes(file.completed_chunks)}</span>
|
||||
</TableCell>
|
||||
<TableCell class="text-center">
|
||||
{
|
||||
let (variant, label) = match file.priority {
|
||||
0 => (BadgeVariant::Destructive, "İndirme"),
|
||||
2 => (BadgeVariant::Success, "Yüksek"),
|
||||
_ => (BadgeVariant::Secondary, "Normal"),
|
||||
};
|
||||
view! { <Badge variant=variant class="text-[10px] uppercase">{label}</Badge> }
|
||||
}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</FileContextMenu>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FileContextMenu(
|
||||
children: Children,
|
||||
torrent_hash: String,
|
||||
file_index: u32,
|
||||
refresh_action: Action<String, Vec<TorrentFile>>,
|
||||
set_priority: Action<(String, u32, u8), Result<(), ServerFnError>>,
|
||||
) -> impl IntoView {
|
||||
let hash_c1 = torrent_hash.clone();
|
||||
let hash_c2 = torrent_hash.clone();
|
||||
let hash_c3 = torrent_hash.clone();
|
||||
|
||||
view! {
|
||||
<ContextMenu>
|
||||
<ContextMenuTrigger>
|
||||
{children()}
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent class="w-48">
|
||||
<ContextMenuLabel>"Dosya Önceliği"</ContextMenuLabel>
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem on:click={
|
||||
let h = hash_c1;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
sp.dispatch((h.clone(), file_index, 2));
|
||||
ra.dispatch(h.clone());
|
||||
crate::components::ui::context_menu::close_context_menu();
|
||||
}
|
||||
}>
|
||||
<icons::ChevronsUp class="text-green-500" />
|
||||
<span>"Yüksek"</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem on:click={
|
||||
let h = hash_c2;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
sp.dispatch((h.clone(), file_index, 1));
|
||||
ra.dispatch(h.clone());
|
||||
crate::components::ui::context_menu::close_context_menu();
|
||||
}
|
||||
}>
|
||||
<icons::Minus class="text-blue-500" />
|
||||
<span>"Normal"</span>
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem class="text-destructive focus:bg-destructive/10" on:click={
|
||||
let h = hash_c3;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
sp.dispatch((h.clone(), file_index, 0));
|
||||
ra.dispatch(h.clone());
|
||||
crate::components::ui::context_menu::close_context_menu();
|
||||
}
|
||||
}>
|
||||
<icons::X />
|
||||
<span>"İndirme (Kapalı)"</span>
|
||||
</ContextMenuItem>
|
||||
</ContextMenuGroup>
|
||||
</ContextMenuContent>
|
||||
</ContextMenu>
|
||||
}
|
||||
}
|
||||
|
||||
#[component]
|
||||
fn FilesFallback() -> impl IntoView {
|
||||
view! {
|
||||
<Shimmer loading=Signal::derive(|| true) class="space-y-2">
|
||||
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
|
||||
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
|
||||
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
|
||||
<div class="h-10 w-full bg-muted/20 rounded-md"></div>
|
||||
</Shimmer>
|
||||
}
|
||||
}
|
||||
|
||||
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])
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod table;
|
||||
pub mod add_torrent;
|
||||
pub mod details;
|
||||
pub mod files;
|
||||
|
||||
@@ -597,10 +597,12 @@ fn TorrentRow(
|
||||
on:click=move |_| store.selected_torrent.set(Some(stored_hash.get_value()))
|
||||
>
|
||||
<DataTableCell class="w-12 px-4">
|
||||
<Checkbox
|
||||
checked=is_selected
|
||||
on_checked_change=on_select
|
||||
/>
|
||||
<div on:click=move |e| e.stop_propagation()>
|
||||
<Checkbox
|
||||
checked=is_selected
|
||||
on_checked_change=on_select
|
||||
/>
|
||||
</div>
|
||||
</DataTableCell>
|
||||
|
||||
{move || visible_columns.get().contains("Name").then({
|
||||
@@ -730,17 +732,23 @@ fn TorrentCard(
|
||||
}
|
||||
)
|
||||
on:click=move |_| {
|
||||
let current = is_selected.get();
|
||||
on_select.run(!current);
|
||||
store.selected_torrent.set(Some(stored_hash.get_value()));
|
||||
}
|
||||
>
|
||||
<div class="p-4 space-y-3">
|
||||
<div class="flex justify-between items-start gap-3">
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-bold leading-tight line-clamp-2 break-all">{t_name.clone()}</h3>
|
||||
<div class="flex items-start gap-3 flex-1 min-w-0">
|
||||
<div on:click=move |e| e.stop_propagation() class="mt-0.5">
|
||||
<Checkbox
|
||||
checked=is_selected
|
||||
on_checked_change=on_select
|
||||
/>
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h3 class="text-sm font-bold leading-tight line-clamp-2 break-all">{t_name.clone()}</h3>
|
||||
</div>
|
||||
</div>
|
||||
<Badge variant=status_variant class="uppercase tracking-wider text-[10px]">
|
||||
<Badge variant=status_variant class="uppercase tracking-wider text-[10px] shrink-0">
|
||||
{format!("{:?}", t.status)}
|
||||
</Badge>
|
||||
</div>
|
||||
|
||||
@@ -139,7 +139,7 @@ pub fn ContextMenuTrigger(
|
||||
class=trigger_class
|
||||
data-name="ContextMenuTrigger"
|
||||
data-context-trigger=ctx.target_id
|
||||
on:contextmenu=move |e: web_sys::MouseEvent| {
|
||||
on:contextmenu=move |_e: web_sys::MouseEvent| {
|
||||
if let Some(cb) = on_open {
|
||||
cb.run(());
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ mod components {
|
||||
clx! {SheetFooter, footer, "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"}
|
||||
}
|
||||
|
||||
pub use components::*;
|
||||
|
||||
/* ========================================================== */
|
||||
/* ✨ CONTEXT ✨ */
|
||||
/* ========================================================== */
|
||||
|
||||
@@ -5,8 +5,7 @@ use leptos::task::spawn_local;
|
||||
use shared::{AppEvent, GlobalStats, NotificationLevel, Torrent};
|
||||
use std::collections::HashMap;
|
||||
use struct_patch::traits::Patch;
|
||||
use base64::{Engine as _, engine::general_purpose::URL_SAFE_NO_PAD as BASE64_URL};
|
||||
use base64::{Engine as _, engine::general_purpose::STANDARD as BASE64};
|
||||
use base64::{Engine as _, engine::general_purpose::{URL_SAFE_NO_PAD as BASE64_URL, STANDARD as BASE64}};
|
||||
use wasm_bindgen::JsCast;
|
||||
|
||||
use crate::components::ui::toast::{ToastType, toast};
|
||||
@@ -192,7 +191,7 @@ pub async fn subscribe_to_push_notifications() {
|
||||
let key_array = js_sys::Uint8Array::from(&decoded_key[..]);
|
||||
|
||||
// 3. Prepare Options
|
||||
let mut options = web_sys::PushSubscriptionOptionsInit::new();
|
||||
let options = web_sys::PushSubscriptionOptionsInit::new();
|
||||
options.set_user_visible_only(true);
|
||||
options.set_application_server_key(&key_array.into());
|
||||
|
||||
|
||||
Reference in New Issue
Block a user