Compare commits

...

7 Commits

Author SHA1 Message Date
spinline
ce212cb2d6 feat: enhance torrent details general tab
All checks were successful
Build MIPS Binary / build (push) Successful in 1m57s
2026-02-21 16:33:10 +03:00
spinline
851d79029a feat(ui): add mobile long-press support for context menus
All checks were successful
Build MIPS Binary / build (push) Successful in 1m58s
2026-02-21 01:46:21 +03:00
spinline
ab27cf3eb4 fix: use f.priority.set instead of deprecated f.set_priority for rTorrent API
All checks were successful
Build MIPS Binary / build (push) Successful in 2m0s
2026-02-21 01:42:07 +03:00
spinline
7b4c9ff336 fix(files): trigger refetch only after set_priority completes on server
All checks were successful
Build MIPS Binary / build (push) Successful in 2m1s
2026-02-21 01:36:21 +03:00
spinline
743596d701 fix(ci): remove tracing calls that caused compile error in shared crate
All checks were successful
Build MIPS Binary / build (push) Successful in 1m58s
2026-02-21 01:31:25 +03:00
spinline
598f038ea6 fix(files): use refetch callback instead of Action to avoid reactive disposal panic
Some checks failed
Build MIPS Binary / build (push) Failing after 1m10s
2026-02-21 01:29:26 +03:00
spinline
7f8c721115 debug: add tracing logs to set_file_priority for diagnosis
Some checks failed
Build MIPS Binary / build (push) Failing after 1m10s
2026-02-21 01:27:22 +03:00
6 changed files with 149 additions and 30 deletions

View File

@@ -51,6 +51,21 @@ mod fields {
pub const IDX_LABEL: usize = 12; pub const IDX_LABEL: usize = 12;
pub const CMD_LABEL: &str = "d.custom1="; pub const CMD_LABEL: &str = "d.custom1=";
pub const IDX_RATIO: usize = 13;
pub const CMD_RATIO: &str = "d.ratio=";
pub const IDX_UPLOADED: usize = 14;
pub const CMD_UPLOADED: &str = "d.up.total=";
pub const IDX_WASTED: usize = 15;
pub const CMD_WASTED: &str = "d.skip.total=";
pub const IDX_SAVE_PATH: usize = 16;
pub const CMD_SAVE_PATH: &str = "d.base_path=";
pub const IDX_FREE_DISK: usize = 17;
pub const CMD_FREE_DISK: &str = "d.free_diskspace=";
} }
use fields::*; use fields::*;
@@ -72,6 +87,11 @@ const RTORRENT_FIELDS: &[&str] = &[
CMD_CREATION_DATE, CMD_CREATION_DATE,
CMD_HASHING, CMD_HASHING,
CMD_LABEL, CMD_LABEL,
CMD_RATIO,
CMD_UPLOADED,
CMD_WASTED,
CMD_SAVE_PATH,
CMD_FREE_DISK,
]; ];
fn parse_long(s: Option<&String>) -> i64 { fn parse_long(s: Option<&String>) -> i64 {
@@ -98,6 +118,11 @@ fn from_rtorrent_row(row: Vec<String>) -> Torrent {
let added_date = parse_long(row.get(IDX_CREATION_DATE)); let added_date = parse_long(row.get(IDX_CREATION_DATE));
let is_hashing = parse_long(row.get(IDX_HASHING)); let is_hashing = parse_long(row.get(IDX_HASHING));
let label_raw = parse_string(row.get(IDX_LABEL)); let label_raw = parse_string(row.get(IDX_LABEL));
let ratio = parse_long(row.get(IDX_RATIO)) as f64 / 1000.0;
let uploaded = parse_long(row.get(IDX_UPLOADED));
let wasted = parse_long(row.get(IDX_WASTED));
let save_path = parse_string(row.get(IDX_SAVE_PATH));
let free_disk_space = parse_long(row.get(IDX_FREE_DISK));
let label = if label_raw.is_empty() { let label = if label_raw.is_empty() {
None None
@@ -144,6 +169,11 @@ fn from_rtorrent_row(row: Vec<String>) -> Torrent {
error_message: message, error_message: message,
added_date, added_date,
label, label,
ratio,
uploaded,
wasted,
save_path,
free_disk_space,
} }
} }

View File

@@ -89,16 +89,59 @@ pub fn TorrentDetailsSheet() -> impl IntoView {
error_message: "".to_string(), error_message: "".to_string(),
added_date: 0, added_date: 0,
label: None, label: None,
ratio: 0.0,
uploaded: 0,
wasted: 0,
save_path: "Yükleniyor...".to_string(),
free_disk_space: 0,
}); });
view! { view! {
<div class="grid grid-cols-2 md:grid-cols-4 gap-6"> <div class="flex flex-col gap-6">
<InfoItem label="İndirilen / Toplam" value=format!("{} / {}", format_bytes(t.completed), format_bytes(t.size)) /> // Aktarım
<InfoItem label="İndirme Hızı" value=format_speed(t.down_rate) class="text-blue-500" /> <div>
<InfoItem label="Gönderme Hızı" value=format_speed(t.up_rate) class="text-green-500" /> <h3 class="text-sm font-bold border-b pb-2 mb-4">"Aktarım"</h3>
<InfoItem label="Eklenme Tarihi" value=format_date(t.added_date) /> <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
<InfoItem label="Kalan Süre" value=format_duration(t.eta) /> <InfoItem label="Geçen Süre" value="N/A".to_string() />
<InfoItem label="Hash" value=t.hash class="col-span-2 md:col-span-4 break-all font-mono text-xs" /> <InfoItem label="Kalan" value=format_duration(t.eta) />
<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="İ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önderme Hızı" value=format_speed(t.up_rate) class="text-green-500" />
<div class="hidden md:block"></div>
<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>
// Genel
<div>
<h3 class="text-sm font-bold border-b pb-2 mb-4">"Genel"</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<InfoItem label="Kaydedilen Yer" value=t.save_path class="col-span-1 md:col-span-2 break-all font-mono text-xs" />
<InfoItem label="Boş Disk Alanı" value=format_bytes(t.free_disk_space) />
<InfoItem label="Oluşturulma Tarihi" value=format_date(t.added_date) />
<InfoItem label="Hash" value=t.hash class="col-span-1 md:col-span-2 break-all font-mono text-xs" />
<InfoItem label="Yorum" value="Yok".to_string() class="col-span-1 md:col-span-2 break-words text-xs" />
</div>
</div>
</div> </div>
} }
}} }}

View File

@@ -15,10 +15,9 @@ pub fn TorrentFilesTab(hash: String) -> impl IntoView {
|h| async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() } |h| async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
); );
// Refresh action // Callback to trigger a refetch — safe, doesn't destroy existing components
let refresh_files = Action::new(|h: &String| { let on_refresh = Callback::new(move |_: ()| {
let h = h.clone(); files_resource.refetch();
async move { shared::server_fns::torrent::get_files(h).await.unwrap_or_default() }
}); });
let stored_hash = StoredValue::new(hash); let stored_hash = StoredValue::new(hash);
@@ -26,10 +25,7 @@ pub fn TorrentFilesTab(hash: String) -> impl IntoView {
view! { view! {
<Suspense fallback=move || view! { <FilesFallback /> }> <Suspense fallback=move || view! { <FilesFallback /> }>
{move || { {move || {
let files = match refresh_files.value().get() { let files = files_resource.get().unwrap_or_default();
Some(f) => f,
None => files_resource.get().unwrap_or_default(),
};
if files.is_empty() { if files.is_empty() {
return view! { return view! {
@@ -61,12 +57,11 @@ pub fn TorrentFilesTab(hash: String) -> impl IntoView {
key=|f| f.index key=|f| f.index
children={move |f| { children={move |f| {
let p_hash = stored_hash.get_value(); let p_hash = stored_hash.get_value();
let r_files = refresh_files.clone();
view! { view! {
<FileRow <FileRow
file=f file=f
hash=p_hash hash=p_hash
refresh_action=r_files on_refresh=on_refresh.clone()
/> />
} }
}} }}
@@ -86,11 +81,14 @@ pub fn TorrentFilesTab(hash: String) -> impl IntoView {
} }
#[component] #[component]
fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<TorrentFile>>) -> impl IntoView { fn FileRow(file: TorrentFile, hash: String, on_refresh: Callback<()>) -> impl IntoView {
let f_idx = file.index; let f_idx = file.index;
let path_clone = file.path.clone(); let path_clone = file.path.clone();
let set_priority = Action::new(|req: &(String, u32, u8)| { // on_refresh is called AFTER the server responds, not before
let on_refresh_stored = StoredValue::new(on_refresh);
let set_priority = Action::new(move |req: &(String, u32, u8)| {
let (h, idx, p) = req.clone(); let (h, idx, p) = req.clone();
async move { async move {
let res = shared::server_fns::torrent::set_file_priority(h, idx, p).await; let res = shared::server_fns::torrent::set_file_priority(h, idx, p).await;
@@ -98,6 +96,8 @@ fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<T
crate::store::show_toast(shared::NotificationLevel::Error, format!("Öncelik değiştirilemedi: {:?}", e)); crate::store::show_toast(shared::NotificationLevel::Error, format!("Öncelik değiştirilemedi: {:?}", e));
} else { } else {
crate::store::show_toast(shared::NotificationLevel::Success, "Dosya önceliği güncellendi.".to_string()); crate::store::show_toast(shared::NotificationLevel::Success, "Dosya önceliği güncellendi.".to_string());
// Refetch AFTER the server has saved the priority
on_refresh_stored.get_value().run(());
} }
res res
} }
@@ -107,7 +107,6 @@ fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<T
<FileContextMenu <FileContextMenu
torrent_hash=hash torrent_hash=hash
file_index=f_idx file_index=f_idx
refresh_action=refresh_action
set_priority=set_priority set_priority=set_priority
> >
<TableRow class="hover:bg-muted/50 transition-colors group"> <TableRow class="hover:bg-muted/50 transition-colors group">
@@ -141,7 +140,6 @@ fn FileContextMenu(
children: Children, children: Children,
torrent_hash: String, torrent_hash: String,
file_index: u32, file_index: u32,
refresh_action: Action<String, Vec<TorrentFile>>,
set_priority: Action<(String, u32, u8), Result<(), ServerFnError>>, set_priority: Action<(String, u32, u8), Result<(), ServerFnError>>,
) -> impl IntoView { ) -> impl IntoView {
let hash_c1 = torrent_hash.clone(); let hash_c1 = torrent_hash.clone();
@@ -159,11 +157,9 @@ fn FileContextMenu(
<ContextMenuGroup> <ContextMenuGroup>
<ContextMenuItem on:click={ <ContextMenuItem on:click={
let h = hash_c1; let h = hash_c1;
let ra = refresh_action.clone();
let sp = set_priority.clone(); let sp = set_priority.clone();
move |_| { move |_| {
sp.dispatch((h.clone(), file_index, 2)); sp.dispatch((h.clone(), file_index, 2));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu(); crate::components::ui::context_menu::close_context_menu();
} }
}> }>
@@ -173,11 +169,9 @@ fn FileContextMenu(
<ContextMenuItem on:click={ <ContextMenuItem on:click={
let h = hash_c2; let h = hash_c2;
let ra = refresh_action.clone();
let sp = set_priority.clone(); let sp = set_priority.clone();
move |_| { move |_| {
sp.dispatch((h.clone(), file_index, 1)); sp.dispatch((h.clone(), file_index, 1));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu(); crate::components::ui::context_menu::close_context_menu();
} }
}> }>
@@ -187,11 +181,9 @@ fn FileContextMenu(
<ContextMenuItem class="text-destructive focus:bg-destructive/10" on:click={ <ContextMenuItem class="text-destructive focus:bg-destructive/10" on:click={
let h = hash_c3; let h = hash_c3;
let ra = refresh_action.clone();
let sp = set_priority.clone(); let sp = set_priority.clone();
move |_| { move |_| {
sp.dispatch((h.clone(), file_index, 0)); sp.dispatch((h.clone(), file_index, 0));
ra.dispatch(h.clone());
crate::components::ui::context_menu::close_context_menu(); crate::components::ui::context_menu::close_context_menu();
} }
}> }>

View File

@@ -296,7 +296,7 @@ pub fn ContextMenuContent(
}} }}
}}; }};
// Right-click on trigger // Right-click on trigger (desktop)
trigger.addEventListener('contextmenu', (e) => {{ trigger.addEventListener('contextmenu', (e) => {{
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
@@ -307,6 +307,53 @@ pub fn ContextMenuContent(
openMenu(e.clientX, e.clientY); openMenu(e.clientX, e.clientY);
}}); }});
// Long-press on trigger (mobile)
let touchTimer = null;
let touchStartX = 0;
let touchStartY = 0;
const LONG_PRESS_DURATION = 500;
const MOVE_THRESHOLD = 10;
trigger.addEventListener('touchstart', (e) => {{
const touch = e.touches[0];
touchStartX = touch.clientX;
touchStartY = touch.clientY;
touchTimer = setTimeout(() => {{
e.preventDefault();
if (isOpen) {{
closeMenu();
}}
openMenu(touchStartX, touchStartY);
}}, LONG_PRESS_DURATION);
}}, {{ passive: false }});
trigger.addEventListener('touchmove', (e) => {{
if (touchTimer) {{
const touch = e.touches[0];
const dx = Math.abs(touch.clientX - touchStartX);
const dy = Math.abs(touch.clientY - touchStartY);
if (dx > MOVE_THRESHOLD || dy > MOVE_THRESHOLD) {{
clearTimeout(touchTimer);
touchTimer = null;
}}
}}
}});
trigger.addEventListener('touchend', () => {{
if (touchTimer) {{
clearTimeout(touchTimer);
touchTimer = null;
}}
}});
trigger.addEventListener('touchcancel', () => {{
if (touchTimer) {{
clearTimeout(touchTimer);
touchTimer = null;
}}
}});
// Close when action is clicked // Close when action is clicked
const actions = menu.querySelectorAll('[data-context-close]'); const actions = menu.querySelectorAll('[data-context-close]');
actions.forEach(action => {{ actions.forEach(action => {{

View File

@@ -42,6 +42,11 @@ pub struct Torrent {
pub error_message: String, pub error_message: String,
pub added_date: i64, pub added_date: i64,
pub label: Option<String>, pub label: Option<String>,
pub ratio: f64,
pub uploaded: i64,
pub wasted: i64,
pub save_path: String,
pub free_disk_space: i64,
} }
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)] #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, ToSchema)]

View File

@@ -225,6 +225,7 @@ pub async fn set_file_priority(
let ctx = expect_context::<crate::ServerContext>(); let ctx = expect_context::<crate::ServerContext>();
let client = RtorrentClient::new(&ctx.scgi_socket_path); let client = RtorrentClient::new(&ctx.scgi_socket_path);
// rTorrent f.set_priority takes: target = "HASH:fINDEX", value = priority
let target = format!("{}:f{}", hash, file_index); let target = format!("{}:f{}", hash, file_index);
let params = vec![ let params = vec![
RpcParam::from(target.as_str()), RpcParam::from(target.as_str()),
@@ -232,10 +233,11 @@ pub async fn set_file_priority(
]; ];
client client
.call("f.set_priority", &params) .call("f.priority.set", &params)
.await .await
.map_err(|e| ServerFnError::new(format!("RPC error: {}", e)))?; .map_err(|e| ServerFnError::new(format!("RPC error setting priority: {}", e)))?;
// Notify rTorrent to update its internal priority state
let _ = client let _ = client
.call("d.update_priorities", &[RpcParam::from(hash.as_str())]) .call("d.update_priorities", &[RpcParam::from(hash.as_str())])
.await; .await;