pinakes: import in parallel; various UI improvements
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I1eb47cd79cd4145c56af966f6756fe1d6a6a6964
This commit is contained in:
parent
278bcaa4b0
commit
116fe7b059
42 changed files with 4316 additions and 316 deletions
|
|
@ -1,3 +1,4 @@
|
|||
use std::collections::HashSet;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::Result;
|
||||
|
|
@ -53,6 +54,9 @@ pub struct AppState {
|
|||
pub page_size: u64,
|
||||
pub total_media_count: u64,
|
||||
pub server_url: String,
|
||||
// Multi-select support
|
||||
pub selected_items: HashSet<String>,
|
||||
pub selection_mode: bool,
|
||||
// Duplicates view
|
||||
pub duplicate_groups: Vec<crate::client::DuplicateGroupResponse>,
|
||||
pub duplicates_selected: Option<usize>,
|
||||
|
|
@ -131,6 +135,9 @@ impl AppState {
|
|||
page_size: 50,
|
||||
total_media_count: 0,
|
||||
server_url: server_url.to_string(),
|
||||
// Multi-select
|
||||
selected_items: HashSet::new(),
|
||||
selection_mode: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1156,6 +1163,154 @@ async fn handle_action(
|
|||
state.current_view = View::Detail;
|
||||
}
|
||||
}
|
||||
Action::ToggleSelection => {
|
||||
// Toggle selection of current item
|
||||
let item_id = match state.current_view {
|
||||
View::Search => state
|
||||
.search_selected
|
||||
.and_then(|i| state.search_results.get(i))
|
||||
.map(|m| m.id.clone()),
|
||||
View::Library => state
|
||||
.selected_index
|
||||
.and_then(|i| state.media_list.get(i))
|
||||
.map(|m| m.id.clone()),
|
||||
_ => None,
|
||||
};
|
||||
if let Some(id) = item_id {
|
||||
if state.selected_items.contains(&id) {
|
||||
state.selected_items.remove(&id);
|
||||
} else {
|
||||
state.selected_items.insert(id);
|
||||
}
|
||||
let count = state.selected_items.len();
|
||||
state.status_message = Some(format!("{} item(s) selected", count));
|
||||
}
|
||||
}
|
||||
Action::SelectAll => {
|
||||
// Select all items in current view
|
||||
let items: Vec<String> = match state.current_view {
|
||||
View::Search => state.search_results.iter().map(|m| m.id.clone()).collect(),
|
||||
View::Library => state.media_list.iter().map(|m| m.id.clone()).collect(),
|
||||
_ => Vec::new(),
|
||||
};
|
||||
for id in items {
|
||||
state.selected_items.insert(id);
|
||||
}
|
||||
let count = state.selected_items.len();
|
||||
state.status_message = Some(format!("{} item(s) selected", count));
|
||||
}
|
||||
Action::ClearSelection => {
|
||||
state.selected_items.clear();
|
||||
state.selection_mode = false;
|
||||
state.status_message = Some("Selection cleared".into());
|
||||
}
|
||||
Action::ToggleSelectionMode => {
|
||||
state.selection_mode = !state.selection_mode;
|
||||
if state.selection_mode {
|
||||
state.status_message =
|
||||
Some("Selection mode: ON (Space to toggle, u to clear)".into());
|
||||
} else {
|
||||
state.status_message = Some("Selection mode: OFF".into());
|
||||
}
|
||||
}
|
||||
Action::BatchDelete => {
|
||||
if state.selected_items.is_empty() {
|
||||
state.status_message = Some("No items selected".into());
|
||||
} else {
|
||||
let count = state.selected_items.len();
|
||||
let ids: Vec<String> = state.selected_items.iter().cloned().collect();
|
||||
state.status_message = Some(format!("Deleting {} item(s)...", count));
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
let page_offset = state.page_offset;
|
||||
let page_size = state.page_size;
|
||||
tokio::spawn(async move {
|
||||
let mut deleted = 0;
|
||||
let mut errors = Vec::new();
|
||||
for id in &ids {
|
||||
match client.delete_media(id).await {
|
||||
Ok(_) => deleted += 1,
|
||||
Err(e) => errors.push(format!("{}: {}", id, e)),
|
||||
}
|
||||
}
|
||||
// Refresh the media list
|
||||
if let Ok(items) = client.list_media(page_offset, page_size).await {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::MediaList(items)));
|
||||
}
|
||||
if errors.is_empty() {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Deleted {} item(s)",
|
||||
deleted
|
||||
))));
|
||||
} else {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Deleted {} item(s), {} error(s)",
|
||||
deleted,
|
||||
errors.len()
|
||||
))));
|
||||
}
|
||||
});
|
||||
state.selected_items.clear();
|
||||
}
|
||||
}
|
||||
Action::BatchTag => {
|
||||
if state.selected_items.is_empty() {
|
||||
state.status_message = Some("No items selected".into());
|
||||
} else if state.all_tags.is_empty() {
|
||||
// Load tags first
|
||||
match client.list_tags().await {
|
||||
Ok(tags) => {
|
||||
state.all_tags = tags;
|
||||
if state.all_tags.is_empty() {
|
||||
state.status_message =
|
||||
Some("No tags available. Create a tag first.".into());
|
||||
} else {
|
||||
state.tag_selected = Some(0);
|
||||
state.status_message = Some(format!(
|
||||
"{} item(s) selected. Press +/- to tag/untag with selected tag.",
|
||||
state.selected_items.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => state.status_message = Some(format!("Failed to load tags: {e}")),
|
||||
}
|
||||
} else if let Some(tag_idx) = state.tag_selected
|
||||
&& let Some(tag) = state.all_tags.get(tag_idx)
|
||||
{
|
||||
let count = state.selected_items.len();
|
||||
let ids: Vec<String> = state.selected_items.iter().cloned().collect();
|
||||
let tag_id = tag.id.clone();
|
||||
let tag_name = tag.name.clone();
|
||||
state.status_message =
|
||||
Some(format!("Tagging {} item(s) with '{}'...", count, tag_name));
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
let mut tagged = 0;
|
||||
let mut errors = Vec::new();
|
||||
for id in &ids {
|
||||
match client.tag_media(id, &tag_id).await {
|
||||
Ok(_) => tagged += 1,
|
||||
Err(e) => errors.push(format!("{}: {}", id, e)),
|
||||
}
|
||||
}
|
||||
if errors.is_empty() {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Tagged {} item(s) with '{}'",
|
||||
tagged, tag_name
|
||||
))));
|
||||
} else {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Tagged {} item(s), {} error(s)",
|
||||
tagged,
|
||||
errors.len()
|
||||
))));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
state.status_message = Some("Select a tag first (use t to view tags)".into());
|
||||
}
|
||||
}
|
||||
Action::NavigateLeft | Action::NavigateRight | Action::None => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue