feat: Implement Context Menu actions and Add Torrent modal

- Implemented Start, Stop, Delete actions in context menu using backend API.
- Replaced  with manual  implementation for context menu reliability.
- Added  for adding torrents via Magnet Link/URL.
- Integrated Add Torrent feature into the Toolbar.
This commit is contained in:
spinline
2026-01-31 20:40:42 +03:00
parent 432bc7b9e9
commit e932fa1e39
6 changed files with 347 additions and 18 deletions

View File

@@ -2,6 +2,8 @@ use leptos::*;
#[component]
pub fn Toolbar() -> impl IntoView {
let (show_add_modal, set_show_add_modal) = create_signal(false);
view! {
<div class="h-14 min-h-14 flex items-center px-4 border-b border-base-300 bg-base-100 gap-4">
<label for="my-drawer" class="btn btn-square btn-ghost lg:hidden drawer-button">
@@ -14,7 +16,11 @@ pub fn Toolbar() -> impl IntoView {
</svg>
"Open"
</button>
<button class="join-item btn btn-sm btn-outline gap-2" title="Magnet Link">
<button
class="join-item btn btn-sm btn-outline gap-2"
title="Magnet Link"
on:click=move |_| set_show_add_modal.set(true)
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-4 h-4">
<path stroke-linecap="round" stroke-linejoin="round" d="M13.19 8.688a4.5 4.5 0 011.242 7.244l-4.5 4.5a4.5 4.5 0 01-6.364-6.364l1.757-1.757m13.35-.622l1.757-1.757a4.5 4.5 0 00-6.364-6.364l-4.5 4.5a4.5 4.5 0 001.242 7.244" />
</svg>
@@ -46,6 +52,10 @@ pub fn Toolbar() -> impl IntoView {
<div class="ml-auto flex items-center gap-2">
<input type="text" placeholder="Filter..." class="input input-sm input-bordered w-full max-w-xs" />
</div>
<Show when=move || show_add_modal.get()>
<crate::components::torrent::add_torrent::AddTorrentModal on_close=move |_| set_show_add_modal.set(false) />
</Show>
</div>
}
}

View File

@@ -0,0 +1,118 @@
use leptos::*;
use leptos::html::Dialog;
#[component]
pub fn AddTorrentModal(
#[prop(into)]
on_close: Callback<()>,
) -> impl IntoView {
let dialog_ref = create_node_ref::<Dialog>();
let (uri, set_uri) = create_signal(String::new());
let (is_loading, set_loading) = create_signal(false);
let (error_msg, set_error_msg) = create_signal(Option::<String>::None);
// Effect to open the dialog when the component mounts/renders
create_effect(move |_| {
if let Some(dialog) = dialog_ref.get() {
let _ = dialog.show_modal();
}
});
let handle_submit = move |_| {
let uri_val = uri.get();
if uri_val.is_empty() {
set_error_msg.set(Some("Please enter a Magnet URI or URL".to_string()));
return;
}
set_loading.set(true);
set_error_msg.set(None);
spawn_local(async move {
let req_body = serde_json::json!({
"uri": uri_val
});
match gloo_net::http::Request::post("/api/torrents/add")
.json(&req_body)
{
Ok(req) => {
match req.send().await {
Ok(resp) => {
if resp.ok() {
logging::log!("Torrent added successfully");
set_loading.set(false);
if let Some(dialog) = dialog_ref.get() {
dialog.close();
}
on_close.call(());
} else {
let status = resp.status();
let text = resp.text().await.unwrap_or_default();
logging::error!("Failed to add torrent: {} - {}", status, text);
set_error_msg.set(Some(format!("Error {}: {}", status, text)));
set_loading.set(false);
}
}
Err(e) => {
logging::error!("Network error: {}", e);
set_error_msg.set(Some(format!("Network Error: {}", e)));
set_loading.set(false);
}
}
}
Err(e) => {
logging::error!("Serialization error: {}", e);
set_error_msg.set(Some(format!("Request Error: {}", e)));
set_loading.set(false);
}
}
});
};
let handle_close = move |_| {
if let Some(dialog) = dialog_ref.get() {
dialog.close();
}
on_close.call(());
};
view! {
<dialog node_ref=dialog_ref class="modal modal-bottom sm:modal-middle" on:close=move |_| on_close.call(())>
<div class="modal-box">
<h3 class="font-bold text-lg">"Add Torrent"</h3>
<p class="py-4">"Enter a Magnet URI or direct URL to a .torrent file."</p>
<div class="form-control w-full">
<input
type="text"
placeholder="magnet:?xt=urn:btih:..."
class="input input-bordered w-full"
prop:value=uri
on:input=move |ev| set_uri.set(event_target_value(&ev))
disabled=is_loading
/>
</div>
<div class="modal-action">
<button class="btn" on:click=handle_close disabled=is_loading>"Cancel"</button>
<button class="btn btn-primary" on:click=handle_submit disabled=is_loading>
{move || if is_loading.get() {
view! { <span class="loading loading-spinner"></span> "Adding..." }.into_view()
} else {
view! { "Add" }.into_view()
}}
</button>
</div>
{move || error_msg.get().map(|msg| view! {
<div class="text-error text-sm mt-2">{msg}</div>
})}
</div>
<form method="dialog" class="modal-backdrop">
<button type="button" on:click=handle_close>"close"</button>
</form>
</dialog>
}
}

View File

@@ -1 +1,2 @@
pub mod table;
pub mod add_torrent;

View File

@@ -119,15 +119,35 @@ pub fn TorrentTable() -> impl IntoView {
let on_action = move |(action, hash): (String, String)| {
logging::log!("TorrentTable Action: {} on {}", action, hash);
// TODO: Implement actual store calls here (start/stop/delete)
match action.as_str() {
"start" => { /* store.start_torrent(&hash) */ },
"stop" => { /* store.stop_torrent(&hash) */ },
"delete" => { /* store.delete_torrent(&hash, false) */ },
"delete_with_data" => { /* store.delete_torrent(&hash, true) */ },
_ => {}
}
set_menu_visible.set(false);
set_menu_visible.set(false); // Close menu immediately
spawn_local(async move {
let action_req = if action == "delete_with_data" { "delete_with_data" } else { &action };
let req_body = shared::TorrentActionRequest {
hash: hash.clone(),
action: action_req.to_string(),
};
let client = gloo_net::http::Request::post("/api/torrents/action")
.json(&req_body);
match client {
Ok(req) => {
match req.send().await {
Ok(resp) => {
if !resp.ok() {
logging::error!("Failed to execute action: {} {}", resp.status(), resp.status_text());
} else {
logging::log!("Action {} executed successfully", action);
}
}
Err(e) => logging::error!("Network error executing action: {}", e),
}
}
Err(e) => logging::error!("Failed to serialize request: {}", e),
}
});
};
view! {
@@ -209,13 +229,15 @@ pub fn TorrentTable() -> impl IntoView {
</tbody>
</table>
<crate::components::context_menu::ContextMenu
visible=menu_visible.get()
position=menu_position.get()
torrent_hash=active_hash.get()
on_close=Callback::from(move |_| set_menu_visible.set(false))
on_action=Callback::from(on_action)
/>
<Show when=move || menu_visible.get() fallback=|| ()>
<crate::components::context_menu::ContextMenu
visible=true
position=menu_position.get()
torrent_hash=active_hash.get()
on_close=Callback::from(move |_| set_menu_visible.set(false))
on_action=Callback::from(on_action)
/>
</Show>
</div>
}
}