various: simplify code; work on security and performance
Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: I9a5114addcab5fbff430ab2b919b83466a6a6964
This commit is contained in:
parent
016841b200
commit
c4adc4e3e0
75 changed files with 12921 additions and 358 deletions
|
|
@ -54,7 +54,7 @@ pub struct AppState {
|
|||
pub total_media_count: u64,
|
||||
pub server_url: String,
|
||||
// Duplicates view
|
||||
pub duplicate_groups: Vec<Vec<crate::client::MediaResponse>>,
|
||||
pub duplicate_groups: Vec<crate::client::DuplicateGroupResponse>,
|
||||
pub duplicates_selected: Option<usize>,
|
||||
// Database view
|
||||
pub database_stats: Option<Vec<(String, String)>>,
|
||||
|
|
@ -249,16 +249,11 @@ fn handle_api_result(state: &mut AppState, result: ApiResult) {
|
|||
}
|
||||
}
|
||||
ApiResult::Duplicates(groups) => {
|
||||
let flat: Vec<Vec<crate::client::MediaResponse>> =
|
||||
groups.into_iter().map(|g| g.items).collect();
|
||||
state.duplicate_groups = flat;
|
||||
if !state.duplicate_groups.is_empty() {
|
||||
if !groups.is_empty() {
|
||||
state.duplicates_selected = Some(0);
|
||||
}
|
||||
state.status_message = Some(format!(
|
||||
"Found {} duplicate groups",
|
||||
state.duplicate_groups.len()
|
||||
));
|
||||
state.status_message = Some(format!("Found {} duplicate groups", groups.len()));
|
||||
state.duplicate_groups = groups;
|
||||
}
|
||||
ApiResult::DatabaseStats(stats) => {
|
||||
state.database_stats = Some(vec![
|
||||
|
|
@ -617,6 +612,13 @@ async fn handle_action(
|
|||
}
|
||||
}
|
||||
}
|
||||
// Also fetch background jobs info
|
||||
match client.list_jobs().await {
|
||||
Ok(jobs) => {
|
||||
tracing::debug!("Found {} background jobs", jobs.len());
|
||||
}
|
||||
Err(e) => tracing::warn!("Failed to list jobs: {}", e),
|
||||
}
|
||||
});
|
||||
}
|
||||
Action::QueueView => {
|
||||
|
|
@ -1024,6 +1026,134 @@ async fn handle_action(
|
|||
"?: Help q: Quit /: Search i: Import o: Open t: Tags c: Collections a: Audit s: Scan S: Settings r: Refresh Home/End: Top/Bottom".into()
|
||||
);
|
||||
}
|
||||
Action::Edit => {
|
||||
if state.current_view == View::Detail
|
||||
&& let Some(ref media) = state.selected_media {
|
||||
// Populate edit fields from selected media
|
||||
state.edit_title = media.title.clone().unwrap_or_default();
|
||||
state.edit_artist = media.artist.clone().unwrap_or_default();
|
||||
state.edit_album = media.album.clone().unwrap_or_default();
|
||||
state.edit_genre = media.genre.clone().unwrap_or_default();
|
||||
state.edit_year = media.year.map(|y| y.to_string()).unwrap_or_default();
|
||||
state.edit_description = media.description.clone().unwrap_or_default();
|
||||
state.edit_field_index = Some(0);
|
||||
state.input_mode = true;
|
||||
state.current_view = View::MetadataEdit;
|
||||
}
|
||||
}
|
||||
Action::Vacuum => {
|
||||
if state.current_view == View::Database {
|
||||
state.status_message = Some("Vacuuming database...".to_string());
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
match client.vacuum_database().await {
|
||||
Ok(()) => {
|
||||
tracing::info!("Database vacuum completed");
|
||||
// Refresh stats after vacuum
|
||||
if let Ok(stats) = client.database_stats().await {
|
||||
let _ =
|
||||
tx.send(AppEvent::ApiResult(ApiResult::DatabaseStats(stats)));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Vacuum failed: {}", e);
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Vacuum failed: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Action::Toggle => {
|
||||
if state.current_view == View::Tasks
|
||||
&& let Some(idx) = state.scheduled_tasks_selected
|
||||
&& let Some(task) = state.scheduled_tasks.get(idx) {
|
||||
let task_id = task.id.clone();
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
match client.toggle_scheduled_task(&task_id).await {
|
||||
Ok(()) => {
|
||||
// Refresh tasks list
|
||||
if let Ok(tasks) = client.list_scheduled_tasks().await {
|
||||
let _ = tx.send(AppEvent::ApiResult(
|
||||
ApiResult::ScheduledTasks(tasks),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to toggle task: {}", e);
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(
|
||||
format!("Toggle task failed: {e}"),
|
||||
)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Action::RunNow => {
|
||||
if state.current_view == View::Tasks
|
||||
&& let Some(idx) = state.scheduled_tasks_selected
|
||||
&& let Some(task) = state.scheduled_tasks.get(idx) {
|
||||
let task_id = task.id.clone();
|
||||
let task_name = task.name.clone();
|
||||
state.status_message = Some(format!("Running task: {task_name}..."));
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
tokio::spawn(async move {
|
||||
match client.run_task_now(&task_id).await {
|
||||
Ok(()) => {
|
||||
// Refresh tasks list
|
||||
if let Ok(tasks) = client.list_scheduled_tasks().await {
|
||||
let _ = tx.send(AppEvent::ApiResult(
|
||||
ApiResult::ScheduledTasks(tasks),
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to run task: {}", e);
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(
|
||||
format!("Run task failed: {e}"),
|
||||
)));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Action::Save => {
|
||||
if state.current_view == View::MetadataEdit
|
||||
&& let Some(ref media) = state.selected_media {
|
||||
let updates = serde_json::json!({
|
||||
"title": if state.edit_title.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_title.clone()) },
|
||||
"artist": if state.edit_artist.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_artist.clone()) },
|
||||
"album": if state.edit_album.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_album.clone()) },
|
||||
"genre": if state.edit_genre.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_genre.clone()) },
|
||||
"year": state.edit_year.parse::<i32>().ok(),
|
||||
"description": if state.edit_description.is_empty() { serde_json::Value::Null } else { serde_json::Value::String(state.edit_description.clone()) },
|
||||
});
|
||||
let media_id = media.id.clone();
|
||||
let client = client.clone();
|
||||
let tx = event_sender.clone();
|
||||
state.status_message = Some("Saving...".to_string());
|
||||
tokio::spawn(async move {
|
||||
match client.update_media(&media_id, updates).await {
|
||||
Ok(_) => {
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::MediaUpdated));
|
||||
}
|
||||
Err(e) => {
|
||||
tracing::error!("Failed to update media: {}", e);
|
||||
let _ = tx.send(AppEvent::ApiResult(ApiResult::Error(format!(
|
||||
"Update failed: {e}"
|
||||
))));
|
||||
}
|
||||
}
|
||||
});
|
||||
state.input_mode = false;
|
||||
state.current_view = View::Detail;
|
||||
}
|
||||
}
|
||||
Action::NavigateLeft | Action::NavigateRight | Action::None => {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,7 +101,10 @@ pub struct DuplicateGroupResponse {
|
|||
pub items: Vec<MediaResponse>,
|
||||
}
|
||||
|
||||
/// Background job response from the API.
|
||||
/// Fields are used for deserialization; the job count is logged in the Database view.
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[allow(dead_code)]
|
||||
pub struct JobResponse {
|
||||
pub id: String,
|
||||
pub kind: serde_json::Value,
|
||||
|
|
|
|||
|
|
@ -36,6 +36,11 @@ pub enum Action {
|
|||
TagMedia,
|
||||
UntagMedia,
|
||||
Help,
|
||||
Edit,
|
||||
Vacuum,
|
||||
Toggle,
|
||||
RunNow,
|
||||
Save,
|
||||
Char(char),
|
||||
Backspace,
|
||||
None,
|
||||
|
|
@ -43,11 +48,15 @@ pub enum Action {
|
|||
|
||||
pub fn handle_key(key: KeyEvent, in_input_mode: bool, current_view: &View) -> Action {
|
||||
if in_input_mode {
|
||||
match key.code {
|
||||
KeyCode::Esc => Action::Back,
|
||||
KeyCode::Enter => Action::Select,
|
||||
KeyCode::Char(c) => Action::Char(c),
|
||||
KeyCode::Backspace => Action::Backspace,
|
||||
match (key.code, key.modifiers) {
|
||||
(KeyCode::Esc, _) => Action::Back,
|
||||
(KeyCode::Enter, _) => Action::Select,
|
||||
(KeyCode::Char('s'), KeyModifiers::CONTROL) => match current_view {
|
||||
View::MetadataEdit => Action::Save,
|
||||
_ => Action::Select,
|
||||
},
|
||||
(KeyCode::Char(c), _) => Action::Char(c),
|
||||
(KeyCode::Backspace, _) => Action::Backspace,
|
||||
_ => Action::None,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -70,10 +79,13 @@ pub fn handle_key(key: KeyEvent, in_input_mode: bool, current_view: &View) -> Ac
|
|||
},
|
||||
(KeyCode::Char('o'), _) => Action::Open,
|
||||
(KeyCode::Char('e'), _) => match current_view {
|
||||
View::Detail => Action::Select,
|
||||
View::Detail => Action::Edit,
|
||||
_ => Action::None,
|
||||
},
|
||||
(KeyCode::Char('t'), _) => Action::TagView,
|
||||
(KeyCode::Char('t'), _) => match current_view {
|
||||
View::Tasks => Action::Toggle,
|
||||
_ => Action::TagView,
|
||||
},
|
||||
(KeyCode::Char('c'), _) => Action::CollectionView,
|
||||
(KeyCode::Char('a'), _) => Action::AuditView,
|
||||
(KeyCode::Char('S'), _) => Action::SettingsView,
|
||||
|
|
@ -82,11 +94,24 @@ pub fn handle_key(key: KeyEvent, in_input_mode: bool, current_view: &View) -> Ac
|
|||
(KeyCode::Char('Q'), _) => Action::QueueView,
|
||||
(KeyCode::Char('X'), _) => Action::StatisticsView,
|
||||
(KeyCode::Char('T'), _) => Action::TasksView,
|
||||
// Ctrl+S must come before plain 's' to ensure proper precedence
|
||||
(KeyCode::Char('s'), KeyModifiers::CONTROL) => match current_view {
|
||||
View::MetadataEdit => Action::Save,
|
||||
_ => Action::None,
|
||||
},
|
||||
(KeyCode::Char('s'), _) => Action::ScanTrigger,
|
||||
(KeyCode::Char('r'), _) => Action::Refresh,
|
||||
(KeyCode::Char('n'), _) => Action::CreateTag,
|
||||
(KeyCode::Char('+'), _) => Action::TagMedia,
|
||||
(KeyCode::Char('-'), _) => Action::UntagMedia,
|
||||
(KeyCode::Char('v'), _) => match current_view {
|
||||
View::Database => Action::Vacuum,
|
||||
_ => Action::None,
|
||||
},
|
||||
(KeyCode::Char('x'), _) => match current_view {
|
||||
View::Tasks => Action::RunNow,
|
||||
_ => Action::None,
|
||||
},
|
||||
(KeyCode::Tab, _) => Action::NextTab,
|
||||
(KeyCode::BackTab, _) => Action::PrevTab,
|
||||
(KeyCode::PageUp, _) => Action::PageUp,
|
||||
|
|
|
|||
|
|
@ -15,14 +15,17 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
} else {
|
||||
let mut list_items = Vec::new();
|
||||
for (i, group) in state.duplicate_groups.iter().enumerate() {
|
||||
// Show truncated hash (first 16 chars) for identification
|
||||
let hash_display = if group.content_hash.len() > 16 {
|
||||
&group.content_hash[..16]
|
||||
} else {
|
||||
&group.content_hash
|
||||
};
|
||||
let header = format!(
|
||||
"Group {} ({} items, hash: {})",
|
||||
"Group {} ({} items, hash: {}...)",
|
||||
i + 1,
|
||||
group.len(),
|
||||
group
|
||||
.first()
|
||||
.map(|m| m.content_hash.as_str())
|
||||
.unwrap_or("?")
|
||||
group.items.len(),
|
||||
hash_display
|
||||
);
|
||||
list_items.push(ListItem::new(Line::from(Span::styled(
|
||||
header,
|
||||
|
|
@ -30,7 +33,7 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
.fg(Color::Yellow)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
))));
|
||||
for item in group {
|
||||
for item in &group.items {
|
||||
let line = format!(" {} - {}", item.file_name, item.path);
|
||||
let is_selected = state
|
||||
.duplicates_selected
|
||||
|
|
|
|||
|
|
@ -37,9 +37,15 @@ pub fn render(f: &mut Frame, state: &AppState, area: Rect) {
|
|||
.map(super::format_date)
|
||||
.unwrap_or("-");
|
||||
let status = task.last_status.as_deref().unwrap_or("-");
|
||||
// Show abbreviated task ID (first 8 chars)
|
||||
let task_id_short = if task.id.len() > 8 {
|
||||
&task.id[..8]
|
||||
} else {
|
||||
&task.id
|
||||
};
|
||||
|
||||
let text = format!(
|
||||
" {enabled_marker} {:<20} {:<16} Last: {:<12} Next: {:<12} Status: {}",
|
||||
" {enabled_marker} [{task_id_short}] {:<20} {:<16} Last: {:<12} Next: {:<12} Status: {}",
|
||||
task.name, task.schedule, last_run, next_run, status
|
||||
);
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue