Compare commits
2 Commits
release-20
...
release-20
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba7f1ffd91 | ||
|
|
daa24dd7ec |
@@ -88,7 +88,6 @@ pub fn TorrentFilesTab(hash: String) -> impl IntoView {
|
||||
#[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)| {
|
||||
@@ -104,42 +103,68 @@ fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<T
|
||||
}
|
||||
});
|
||||
|
||||
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 attr:id=context_id.clone()>
|
||||
<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>
|
||||
<ContextMenuTrigger>
|
||||
{children()}
|
||||
</ContextMenuTrigger>
|
||||
|
||||
<ContextMenuContent class="w-48">
|
||||
<ContextMenuLabel>"Dosya Önceliği"</ContextMenuLabel>
|
||||
<ContextMenuGroup>
|
||||
<ContextMenuItem on:click={
|
||||
let h = hash.clone();
|
||||
let h = hash_c1;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
set_priority.dispatch((h.clone(), f_idx, 2));
|
||||
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" />
|
||||
@@ -147,11 +172,13 @@ fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<T
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem on:click={
|
||||
let h = hash.clone();
|
||||
let h = hash_c2;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
set_priority.dispatch((h.clone(), f_idx, 1));
|
||||
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" />
|
||||
@@ -159,11 +186,13 @@ fn FileRow(file: TorrentFile, hash: String, refresh_action: Action<String, Vec<T
|
||||
</ContextMenuItem>
|
||||
|
||||
<ContextMenuItem class="text-destructive focus:bg-destructive/10" on:click={
|
||||
let h = hash.clone();
|
||||
let h = hash_c3;
|
||||
let ra = refresh_action.clone();
|
||||
let sp = set_priority.clone();
|
||||
move |_| {
|
||||
set_priority.dispatch((h.clone(), f_idx, 0));
|
||||
sp.dispatch((h.clone(), file_index, 0));
|
||||
ra.dispatch(h.clone());
|
||||
crate::components::ui::context_menu::close_context_menu();
|
||||
}
|
||||
}>
|
||||
<icons::X />
|
||||
|
||||
@@ -215,12 +215,20 @@ pub fn ContextMenuContent(
|
||||
|
||||
// Adjust if menu would go off right edge
|
||||
if (x + menuRect.width > viewportWidth) {{
|
||||
left = x - menuRect.width;
|
||||
left = Math.max(0, x - menuRect.width);
|
||||
}}
|
||||
|
||||
// Adjust if menu would go off bottom edge
|
||||
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`;
|
||||
@@ -228,105 +236,105 @@ pub fn ContextMenuContent(
|
||||
menu.style.transformOrigin = 'top left';
|
||||
}};
|
||||
|
||||
const openMenu = (x, y) => {{
|
||||
isOpen = true;
|
||||
const openMenu = (x, y) => {{
|
||||
isOpen = true;
|
||||
|
||||
// Close any other open context menus
|
||||
const allMenus = document.querySelectorAll('[data-target="target__context"]');
|
||||
allMenus.forEach(m => {{
|
||||
if (m !== menu && m.getAttribute('data-state') === 'open') {{
|
||||
m.setAttribute('data-state', 'closed');
|
||||
m.style.pointerEvents = 'none';
|
||||
// Close any other open context menus
|
||||
const allMenus = document.querySelectorAll('[data-target="target__context"]');
|
||||
allMenus.forEach(m => {{
|
||||
if (m !== menu && m.getAttribute('data-state') === 'open') {{
|
||||
m.setAttribute('data-state', 'closed');
|
||||
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 = () => {{
|
||||
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();
|
||||
}}
|
||||
}});
|
||||
}};
|
||||
|
||||
if (document.readyState === 'loading') {{
|
||||
document.addEventListener('DOMContentLoaded', setupContextMenu);
|
||||
}} else {{
|
||||
setupContextMenu();
|
||||
}}
|
||||
}})();
|
||||
"#,
|
||||
target_id_for_script,
|
||||
target_id_for_script,
|
||||
)}
|
||||
</script>
|
||||
if (document.readyState === 'loading') {{
|
||||
document.addEventListener('DOMContentLoaded', setupContextMenu);
|
||||
}} else {{
|
||||
setupContextMenu();
|
||||
}}
|
||||
}})();
|
||||
"#,
|
||||
target_id_for_script,
|
||||
target_id_for_script,
|
||||
)}
|
||||
</script>
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user