Compare commits
4 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
743596d701 | ||
|
|
598f038ea6 | ||
|
|
7f8c721115 | ||
|
|
ba7f1ffd91 |
@@ -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,9 +81,8 @@ 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 context_id = format!("file-context-{}-{}", hash, f_idx);
|
|
||||||
let path_clone = file.path.clone();
|
let path_clone = file.path.clone();
|
||||||
|
|
||||||
let set_priority = Action::new(|req: &(String, u32, u8)| {
|
let set_priority = Action::new(|req: &(String, u32, u8)| {
|
||||||
@@ -108,7 +102,7 @@ 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
|
on_refresh=on_refresh
|
||||||
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">
|
||||||
@@ -142,7 +136,7 @@ fn FileContextMenu(
|
|||||||
children: Children,
|
children: Children,
|
||||||
torrent_hash: String,
|
torrent_hash: String,
|
||||||
file_index: u32,
|
file_index: u32,
|
||||||
refresh_action: Action<String, Vec<TorrentFile>>,
|
on_refresh: Callback<()>,
|
||||||
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();
|
||||||
@@ -160,11 +154,11 @@ 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();
|
||||||
|
let ra = on_refresh.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
sp.dispatch((h.clone(), file_index, 2));
|
sp.dispatch((h.clone(), file_index, 2));
|
||||||
ra.dispatch(h.clone());
|
ra.run(());
|
||||||
crate::components::ui::context_menu::close_context_menu();
|
crate::components::ui::context_menu::close_context_menu();
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
@@ -174,11 +168,11 @@ 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();
|
||||||
|
let ra = on_refresh.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
sp.dispatch((h.clone(), file_index, 1));
|
sp.dispatch((h.clone(), file_index, 1));
|
||||||
ra.dispatch(h.clone());
|
ra.run(());
|
||||||
crate::components::ui::context_menu::close_context_menu();
|
crate::components::ui::context_menu::close_context_menu();
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
@@ -188,11 +182,11 @@ 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();
|
||||||
|
let ra = on_refresh.clone();
|
||||||
move |_| {
|
move |_| {
|
||||||
sp.dispatch((h.clone(), file_index, 0));
|
sp.dispatch((h.clone(), file_index, 0));
|
||||||
ra.dispatch(h.clone());
|
ra.run(());
|
||||||
crate::components::ui::context_menu::close_context_menu();
|
crate::components::ui::context_menu::close_context_menu();
|
||||||
}
|
}
|
||||||
}>
|
}>
|
||||||
|
|||||||
@@ -215,12 +215,20 @@ pub fn ContextMenuContent(
|
|||||||
|
|
||||||
// Adjust if menu would go off right edge
|
// Adjust if menu would go off right edge
|
||||||
if (x + menuRect.width > viewportWidth) {{
|
if (x + menuRect.width > viewportWidth) {{
|
||||||
left = x - menuRect.width;
|
left = Math.max(0, x - menuRect.width);
|
||||||
}}
|
}}
|
||||||
|
|
||||||
// Adjust if menu would go off bottom edge
|
// Adjust if menu would go off bottom edge
|
||||||
if (y + menuRect.height > viewportHeight) {{
|
if (y + menuRect.height > viewportHeight) {{
|
||||||
top = y - menuRect.height;
|
top = Math.max(0, y - menuRect.height);
|
||||||
|
}}
|
||||||
|
|
||||||
|
// Adjust for CSS transformed containing block
|
||||||
|
const offsetParent = menu.offsetParent;
|
||||||
|
if (offsetParent && offsetParent !== document.body && offsetParent !== document.documentElement) {{
|
||||||
|
const parentRect = offsetParent.getBoundingClientRect();
|
||||||
|
left -= parentRect.left;
|
||||||
|
top -= parentRect.top;
|
||||||
}}
|
}}
|
||||||
|
|
||||||
menu.style.left = `${{left}}px`;
|
menu.style.left = `${{left}}px`;
|
||||||
@@ -228,105 +236,105 @@ pub fn ContextMenuContent(
|
|||||||
menu.style.transformOrigin = 'top left';
|
menu.style.transformOrigin = 'top left';
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const openMenu = (x, y) => {{
|
const openMenu = (x, y) => {{
|
||||||
isOpen = true;
|
isOpen = true;
|
||||||
|
|
||||||
// Close any other open context menus
|
// Close any other open context menus
|
||||||
const allMenus = document.querySelectorAll('[data-target="target__context"]');
|
const allMenus = document.querySelectorAll('[data-target="target__context"]');
|
||||||
allMenus.forEach(m => {{
|
allMenus.forEach(m => {{
|
||||||
if (m !== menu && m.getAttribute('data-state') === 'open') {{
|
if (m !== menu && m.getAttribute('data-state') === 'open') {{
|
||||||
m.setAttribute('data-state', 'closed');
|
m.setAttribute('data-state', 'closed');
|
||||||
m.style.pointerEvents = 'none';
|
m.style.pointerEvents = 'none';
|
||||||
|
}}
|
||||||
|
}});
|
||||||
|
|
||||||
|
menu.setAttribute('data-state', 'open');
|
||||||
|
menu.style.visibility = 'hidden';
|
||||||
|
menu.style.pointerEvents = 'auto';
|
||||||
|
|
||||||
|
// Force reflow
|
||||||
|
menu.offsetHeight;
|
||||||
|
|
||||||
|
updatePosition(x, y);
|
||||||
|
menu.style.visibility = 'visible';
|
||||||
|
|
||||||
|
// Lock scroll
|
||||||
|
if (window.ScrollLock) {{
|
||||||
|
window.ScrollLock.lock();
|
||||||
|
}}
|
||||||
|
|
||||||
|
setTimeout(() => {{
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
document.addEventListener('contextmenu', handleContextOutside);
|
||||||
|
}}, 0);
|
||||||
|
}};
|
||||||
|
|
||||||
|
const closeMenu = () => {{
|
||||||
|
isOpen = false;
|
||||||
|
menu.setAttribute('data-state', 'closed');
|
||||||
|
menu.style.pointerEvents = 'none';
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
document.removeEventListener('contextmenu', handleContextOutside);
|
||||||
|
|
||||||
|
// Dispatch custom event for Leptos to listen to
|
||||||
|
menu.dispatchEvent(new CustomEvent('contextmenuclose', {{ bubbles: false }}));
|
||||||
|
|
||||||
|
if (window.ScrollLock) {{
|
||||||
|
window.ScrollLock.unlock(200);
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
const handleClickOutside = (e) => {{
|
||||||
|
if (!menu.contains(e.target)) {{
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
const handleContextOutside = (e) => {{
|
||||||
|
if (!trigger.contains(e.target)) {{
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
}};
|
||||||
|
|
||||||
|
// Right-click on trigger
|
||||||
|
trigger.addEventListener('contextmenu', (e) => {{
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
if (isOpen) {{
|
||||||
|
closeMenu();
|
||||||
|
}}
|
||||||
|
openMenu(e.clientX, e.clientY);
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Close when action is clicked
|
||||||
|
const actions = menu.querySelectorAll('[data-context-close]');
|
||||||
|
actions.forEach(action => {{
|
||||||
|
action.addEventListener('click', () => {{
|
||||||
|
closeMenu();
|
||||||
|
}});
|
||||||
|
}});
|
||||||
|
|
||||||
|
// Handle ESC key
|
||||||
|
document.addEventListener('keydown', (e) => {{
|
||||||
|
if (e.key === 'Escape' && isOpen) {{
|
||||||
|
e.preventDefault();
|
||||||
|
closeMenu();
|
||||||
}}
|
}}
|
||||||
}});
|
}});
|
||||||
|
|
||||||
menu.setAttribute('data-state', 'open');
|
|
||||||
menu.style.visibility = 'hidden';
|
|
||||||
menu.style.pointerEvents = 'auto';
|
|
||||||
|
|
||||||
// Force reflow
|
|
||||||
menu.offsetHeight;
|
|
||||||
|
|
||||||
updatePosition(x, y);
|
|
||||||
menu.style.visibility = 'visible';
|
|
||||||
|
|
||||||
// Lock scroll
|
|
||||||
if (window.ScrollLock) {{
|
|
||||||
window.ScrollLock.lock();
|
|
||||||
}}
|
|
||||||
|
|
||||||
setTimeout(() => {{
|
|
||||||
document.addEventListener('click', handleClickOutside);
|
|
||||||
document.addEventListener('contextmenu', handleContextOutside);
|
|
||||||
}}, 0);
|
|
||||||
}};
|
}};
|
||||||
|
|
||||||
const closeMenu = () => {{
|
if (document.readyState === 'loading') {{
|
||||||
isOpen = false;
|
document.addEventListener('DOMContentLoaded', setupContextMenu);
|
||||||
menu.setAttribute('data-state', 'closed');
|
}} else {{
|
||||||
menu.style.pointerEvents = 'none';
|
setupContextMenu();
|
||||||
document.removeEventListener('click', handleClickOutside);
|
}}
|
||||||
document.removeEventListener('contextmenu', handleContextOutside);
|
}})();
|
||||||
|
"#,
|
||||||
// Dispatch custom event for Leptos to listen to
|
target_id_for_script,
|
||||||
menu.dispatchEvent(new CustomEvent('contextmenuclose', {{ bubbles: false }}));
|
target_id_for_script,
|
||||||
|
)}
|
||||||
if (window.ScrollLock) {{
|
</script>
|
||||||
window.ScrollLock.unlock(200);
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
|
|
||||||
const handleClickOutside = (e) => {{
|
|
||||||
if (!menu.contains(e.target)) {{
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
|
|
||||||
const handleContextOutside = (e) => {{
|
|
||||||
if (!trigger.contains(e.target)) {{
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
}};
|
|
||||||
|
|
||||||
// Right-click on trigger
|
|
||||||
trigger.addEventListener('contextmenu', (e) => {{
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (isOpen) {{
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
openMenu(e.clientX, e.clientY);
|
|
||||||
}});
|
|
||||||
|
|
||||||
// Close when action is clicked
|
|
||||||
const actions = menu.querySelectorAll('[data-context-close]');
|
|
||||||
actions.forEach(action => {{
|
|
||||||
action.addEventListener('click', () => {{
|
|
||||||
closeMenu();
|
|
||||||
}});
|
|
||||||
}});
|
|
||||||
|
|
||||||
// Handle ESC key
|
|
||||||
document.addEventListener('keydown', (e) => {{
|
|
||||||
if (e.key === 'Escape' && isOpen) {{
|
|
||||||
e.preventDefault();
|
|
||||||
closeMenu();
|
|
||||||
}}
|
|
||||||
}});
|
|
||||||
}};
|
|
||||||
|
|
||||||
if (document.readyState === 'loading') {{
|
|
||||||
document.addEventListener('DOMContentLoaded', setupContextMenu);
|
|
||||||
}} else {{
|
|
||||||
setupContextMenu();
|
|
||||||
}}
|
|
||||||
}})();
|
|
||||||
"#,
|
|
||||||
target_id_for_script,
|
|
||||||
target_id_for_script,
|
|
||||||
)}
|
|
||||||
</script>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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()),
|
||||||
@@ -234,8 +235,9 @@ pub async fn set_file_priority(
|
|||||||
client
|
client
|
||||||
.call("f.set_priority", ¶ms)
|
.call("f.set_priority", ¶ms)
|
||||||
.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;
|
||||||
|
|||||||
Reference in New Issue
Block a user