feat: implement trackers tab with details
All checks were successful
Build MIPS Binary / build (push) Successful in 2m1s
All checks were successful
Build MIPS Binary / build (push) Successful in 2m1s
This commit is contained in:
@@ -165,10 +165,19 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
|
|||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="trackers" class="h-full">
|
<TabsContent value="trackers" class="h-full">
|
||||||
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
{move || match selected_torrent.get() {
|
||||||
<icons::Settings2 class="size-12 mb-3 text-muted-foreground" />
|
Some(t) => leptos::either::Either::Left(view! {
|
||||||
<p class="text-sm font-medium">"İzleyici listesi yakında eklenecek"</p>
|
<div class="h-full overflow-y-auto pr-2 pb-8">
|
||||||
</div>
|
<crate::components::torrent::trackers::TorrentTrackersTab hash=t.hash />
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
None => leptos::either::Either::Right(view! {
|
||||||
|
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
||||||
|
<icons::Settings2 class="size-12 mb-3 text-muted-foreground" />
|
||||||
|
<p class="text-sm font-medium">"İzleyici yükleniyor..."</p>
|
||||||
|
</div>
|
||||||
|
}),
|
||||||
|
}}
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="peers" class="h-full">
|
<TabsContent value="peers" class="h-full">
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ pub mod table;
|
|||||||
pub mod add_torrent;
|
pub mod add_torrent;
|
||||||
pub mod details;
|
pub mod details;
|
||||||
pub mod files;
|
pub mod files;
|
||||||
|
pub mod trackers;
|
||||||
|
|||||||
121
frontend/src/components/torrent/trackers.rs
Normal file
121
frontend/src/components/torrent/trackers.rs
Normal file
@@ -0,0 +1,121 @@
|
|||||||
|
use leptos::prelude::*;
|
||||||
|
use crate::components::ui::table::*;
|
||||||
|
use crate::components::ui::shimmer::*;
|
||||||
|
use shared::TorrentTracker;
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
pub fn TorrentTrackersTab(hash: String) -> impl IntoView {
|
||||||
|
let hash_clone = hash.clone();
|
||||||
|
|
||||||
|
let trackers_resource = Resource::new(
|
||||||
|
move || hash_clone.clone(),
|
||||||
|
|h| async move { shared::server_fns::torrent::get_trackers(h).await.unwrap_or_default() }
|
||||||
|
);
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Suspense fallback=move || view! { <TrackersFallback /> }>
|
||||||
|
{move || {
|
||||||
|
let trackers = trackers_resource.get().unwrap_or_default();
|
||||||
|
|
||||||
|
if trackers.is_empty() {
|
||||||
|
return view! {
|
||||||
|
<div class="flex flex-col items-center justify-center h-48 opacity-60">
|
||||||
|
<icons::Settings2 class="size-12 mb-3 text-muted-foreground" />
|
||||||
|
<p class="text-sm font-medium">"Bu torrent için izleyici bulunamadı."</p>
|
||||||
|
</div>
|
||||||
|
}.into_any();
|
||||||
|
}
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="h-full overflow-auto">
|
||||||
|
<TableWrapper class="bg-card/50 whitespace-nowrap">
|
||||||
|
<Table>
|
||||||
|
<TableHeader class="sticky top-0 bg-muted/80 backdrop-blur-sm z-10">
|
||||||
|
<TableRow class="hover:bg-transparent text-xs">
|
||||||
|
<TableHead>"İsim"</TableHead>
|
||||||
|
<TableHead class="text-center">"Tür"</TableHead>
|
||||||
|
<TableHead class="text-center">"Etkin"</TableHead>
|
||||||
|
<TableHead class="text-center">"Grup"</TableHead>
|
||||||
|
<TableHead class="text-center">"Ortaklar"</TableHead>
|
||||||
|
<TableHead class="text-center">"Eşler"</TableHead>
|
||||||
|
<TableHead class="text-center">"İndirilen"</TableHead>
|
||||||
|
<TableHead class="text-center">"Son Güncelleme"</TableHead>
|
||||||
|
<TableHead class="text-center">"Sıklık"</TableHead>
|
||||||
|
<TableHead class="text-center">"Özel"</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
<For
|
||||||
|
each=move || trackers.clone()
|
||||||
|
key=|t| t.url.clone()
|
||||||
|
children=move |t| {
|
||||||
|
let t_type = if t.url.starts_with("http") { "http" }
|
||||||
|
else if t.url.starts_with("udp") { "udp" }
|
||||||
|
else if t.url.starts_with("dht") { "dht" }
|
||||||
|
else { "diğer" };
|
||||||
|
let is_enabled = if t.is_enabled { "evet" } else { "hayır" };
|
||||||
|
|
||||||
|
// Format timestamp difference for last update
|
||||||
|
let now = chrono::Utc::now().timestamp();
|
||||||
|
let diff = now - t.last_updated;
|
||||||
|
let last_update_str = if t.last_updated == 0 {
|
||||||
|
"Güncellenmedi".to_string()
|
||||||
|
} else if diff >= 0 {
|
||||||
|
format_duration_short(diff)
|
||||||
|
} else {
|
||||||
|
"N/A".to_string()
|
||||||
|
};
|
||||||
|
|
||||||
|
let url_clone = t.url.clone();
|
||||||
|
view! {
|
||||||
|
<TableRow class="hover:bg-muted/50 transition-colors group text-xs text-muted-foreground">
|
||||||
|
<TableCell class="font-medium text-foreground max-w-[200px] md:max-w-md truncate" attr:title=url_clone>
|
||||||
|
{t.url.clone()}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell class="text-center">{t_type}</TableCell>
|
||||||
|
<TableCell class="text-center">{is_enabled}</TableCell>
|
||||||
|
<TableCell class="text-center">{t.group}</TableCell>
|
||||||
|
<TableCell class="text-center">{t.seeders}</TableCell>
|
||||||
|
<TableCell class="text-center">{t.peers}</TableCell>
|
||||||
|
<TableCell class="text-center">{t.downloaded}</TableCell>
|
||||||
|
<TableCell class="text-center">{last_update_str}</TableCell>
|
||||||
|
<TableCell class="text-center">{format_duration_short(t.interval)}</TableCell>
|
||||||
|
<TableCell class="text-center">"bilinmiyor"</TableCell> // Özel flag isn't cleanly via XMLRPC per tracker usually
|
||||||
|
</TableRow>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</TableWrapper>
|
||||||
|
</div>
|
||||||
|
}.into_any()
|
||||||
|
}}
|
||||||
|
</Suspense>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn TrackersFallback() -> 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_duration_short(seconds: i64) -> String {
|
||||||
|
if seconds <= 0 { return "0sn".to_string(); }
|
||||||
|
let days = seconds / 86400;
|
||||||
|
let hours = (seconds % 86400) / 3600;
|
||||||
|
let minutes = (seconds % 3600) / 60;
|
||||||
|
let secs = seconds % 60;
|
||||||
|
|
||||||
|
if days > 0 { format!("{}g {}s", days, hours) }
|
||||||
|
else if hours > 0 { format!("{}s {}dk", hours, minutes) }
|
||||||
|
else if minutes > 0 { format!("{}dk {}sn", minutes, secs) }
|
||||||
|
else { format!("{}sn", secs) }
|
||||||
|
}
|
||||||
@@ -126,6 +126,13 @@ pub struct TorrentTracker {
|
|||||||
pub url: String,
|
pub url: String,
|
||||||
pub status: String,
|
pub status: String,
|
||||||
pub message: String,
|
pub message: String,
|
||||||
|
pub is_enabled: bool,
|
||||||
|
pub group: i64,
|
||||||
|
pub seeders: i64,
|
||||||
|
pub peers: i64,
|
||||||
|
pub downloaded: i64,
|
||||||
|
pub last_updated: i64,
|
||||||
|
pub interval: i64,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
#[derive(Debug, Serialize, Deserialize, Clone, ToSchema)]
|
||||||
|
|||||||
@@ -193,7 +193,13 @@ pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnE
|
|||||||
RpcParam::from(hash.as_str()),
|
RpcParam::from(hash.as_str()),
|
||||||
RpcParam::from(""),
|
RpcParam::from(""),
|
||||||
RpcParam::from("t.url="),
|
RpcParam::from("t.url="),
|
||||||
|
RpcParam::from("t.is_enabled="),
|
||||||
|
RpcParam::from("t.group="),
|
||||||
|
RpcParam::from("t.scrape_complete="),
|
||||||
|
RpcParam::from("t.scrape_incomplete="),
|
||||||
|
RpcParam::from("t.scrape_downloaded="),
|
||||||
RpcParam::from("t.activity_date_last="),
|
RpcParam::from("t.activity_date_last="),
|
||||||
|
RpcParam::from("t.normal_interval="),
|
||||||
RpcParam::from("t.message="),
|
RpcParam::from("t.message="),
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -209,8 +215,15 @@ pub async fn get_trackers(hash: String) -> Result<Vec<TorrentTracker>, ServerFnE
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|row| TorrentTracker {
|
.map(|row| TorrentTracker {
|
||||||
url: row.get(0).cloned().unwrap_or_default(),
|
url: row.get(0).cloned().unwrap_or_default(),
|
||||||
status: "Unknown".to_string(),
|
is_enabled: row.get(1).and_then(|s| s.parse::<i64>().ok()).unwrap_or(0) != 0,
|
||||||
message: row.get(2).cloned().unwrap_or_default(),
|
group: row.get(2).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
seeders: row.get(3).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
peers: row.get(4).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
downloaded: row.get(5).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
last_updated: row.get(6).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
interval: row.get(7).and_then(|s| s.parse().ok()).unwrap_or(0),
|
||||||
|
status: "Unknown".to_string(), // Can derive from message or activity later, or keep unknown
|
||||||
|
message: row.get(8).cloned().unwrap_or_default(),
|
||||||
})
|
})
|
||||||
.collect())
|
.collect())
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user