diff --git a/Cargo.toml b/Cargo.toml index d51f61c..773089b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cm-player" -version = "0.1.11" +version = "0.1.12" edition = "2021" [dependencies] diff --git a/src/main.rs b/src/main.rs index c3fe5df..1eae410 100644 --- a/src/main.rs +++ b/src/main.rs @@ -339,29 +339,32 @@ async fn handle_key_event(terminal: &mut Terminal< // Next track if !state.playlist.is_empty() && state.playlist_index + 1 < state.playlist.len() { state.playlist_index += 1; - state.current_file = Some(state.playlist[state.playlist_index].clone()); + // Validate index before accessing playlist + if state.playlist_index < state.playlist.len() { + state.current_file = Some(state.playlist[state.playlist_index].clone()); - match state.player_state { - PlayerState::Playing => { - // Keep playing - if let Some(ref path) = state.current_file { - player.play(path)?; - player.update_metadata(); // Update metadata immediately - tracing::info!("Next track: {:?}", path); + match state.player_state { + PlayerState::Playing => { + // Keep playing + if let Some(ref path) = state.current_file { + player.play(path)?; + player.update_metadata(); // Update metadata immediately + tracing::info!("Next track: {:?}", path); + } } - } - PlayerState::Paused => { - // Load but stay paused - if let Some(ref path) = state.current_file { - player.play(path)?; - player.update_metadata(); // Update metadata immediately - player.pause()?; - tracing::info!("Next track (paused): {:?}", path); + PlayerState::Paused => { + // Load but stay paused + if let Some(ref path) = state.current_file { + player.play(path)?; + player.update_metadata(); // Update metadata immediately + player.pause()?; + tracing::info!("Next track (paused): {:?}", path); + } + } + PlayerState::Stopped => { + // Just update current file, stay stopped + tracing::info!("Next track selected (stopped): {:?}", state.current_file); } - } - PlayerState::Stopped => { - // Just update current file, stay stopped - tracing::info!("Next track selected (stopped): {:?}", state.current_file); } } } @@ -370,29 +373,32 @@ async fn handle_key_event(terminal: &mut Terminal< // Previous track if !state.playlist.is_empty() && state.playlist_index > 0 { state.playlist_index -= 1; - state.current_file = Some(state.playlist[state.playlist_index].clone()); + // Validate index before accessing playlist + if state.playlist_index < state.playlist.len() { + state.current_file = Some(state.playlist[state.playlist_index].clone()); - match state.player_state { - PlayerState::Playing => { - // Keep playing - if let Some(ref path) = state.current_file { - player.play(path)?; - player.update_metadata(); // Update metadata immediately - tracing::info!("Previous track: {:?}", path); + match state.player_state { + PlayerState::Playing => { + // Keep playing + if let Some(ref path) = state.current_file { + player.play(path)?; + player.update_metadata(); // Update metadata immediately + tracing::info!("Previous track: {:?}", path); + } } - } - PlayerState::Paused => { - // Load but stay paused - if let Some(ref path) = state.current_file { - player.play(path)?; - player.update_metadata(); // Update metadata immediately - player.pause()?; - tracing::info!("Previous track (paused): {:?}", path); + PlayerState::Paused => { + // Load but stay paused + if let Some(ref path) = state.current_file { + player.play(path)?; + player.update_metadata(); // Update metadata immediately + player.pause()?; + tracing::info!("Previous track (paused): {:?}", path); + } + } + PlayerState::Stopped => { + // Just update current file, stay stopped + tracing::info!("Previous track selected (stopped): {:?}", state.current_file); } - } - PlayerState::Stopped => { - // Just update current file, stay stopped - tracing::info!("Previous track selected (stopped): {:?}", state.current_file); } } } diff --git a/src/state/mod.rs b/src/state/mod.rs index d56983b..4a59dc5 100644 --- a/src/state/mod.rs +++ b/src/state/mod.rs @@ -271,6 +271,10 @@ impl AppState { if let Some(first) = self.playlist.first() { self.current_file = Some(first.clone()); self.player_state = PlayerState::Playing; + } else { + // Empty playlist + self.current_file = None; + self.player_state = PlayerState::Stopped; } } else if let Some(item) = self.get_selected_item() { let node = item.node.clone(); @@ -281,6 +285,10 @@ impl AppState { if let Some(first) = self.playlist.first() { self.current_file = Some(first.clone()); self.player_state = PlayerState::Playing; + } else { + // Empty directory + self.current_file = None; + self.player_state = PlayerState::Stopped; } } else { // Play single file @@ -296,8 +304,11 @@ impl AppState { pub fn play_next(&mut self) { if self.playlist_index + 1 < self.playlist.len() { self.playlist_index += 1; - self.current_file = Some(self.playlist[self.playlist_index].clone()); - self.player_state = PlayerState::Playing; + // Double-check index is valid before accessing + if self.playlist_index < self.playlist.len() { + self.current_file = Some(self.playlist[self.playlist_index].clone()); + self.player_state = PlayerState::Playing; + } } } diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 289360b..6bd3b9c 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -48,7 +48,7 @@ pub fn render(frame: &mut Frame, state: &mut AppState, player: &Player) -> (Rect (main_chunks[0], content_chunks[0]) } -fn highlight_search_matches<'a>(text: &str, query: &str, search_typing: bool, is_selected: bool) -> Vec> { +fn highlight_search_matches<'a>(text: &str, query: &str, is_selected: bool) -> Vec> { let query_lower = query.to_lowercase(); let mut spans = Vec::new(); @@ -66,17 +66,9 @@ fn highlight_search_matches<'a>(text: &str, query: &str, search_typing: bool, is spans.push(Span::raw(current_segment.clone())); current_segment.clear(); } - // Add matched character with styling based on mode - if search_typing && is_selected { - // While typing on selected row: green bg with black fg (inverted) - spans.push(Span::styled( - ch.to_string(), - Style::default() - .fg(Theme::background()) - .bg(Theme::success()), - )); - } else if !search_typing && is_selected { - // After Enter on selected row: bold black text on blue selection bar + // Add matched character with styling + if is_selected { + // On selected row: bold black text on selection bar (yellow or blue) spans.push(Span::styled( ch.to_string(), Style::default() @@ -116,7 +108,6 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) { state.update_scroll_offset(visible_height); let in_search = state.search_mode || !state.search_matches.is_empty(); - let search_typing = state.search_mode; // True when actively typing search let search_query = if in_search { state.search_query.to_lowercase() } else { String::new() }; let items: Vec = state @@ -133,7 +124,7 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) { // Add folder icon for directories let icon = if item.node.is_dir { // Bold black icon on selection bar, blue otherwise - if !search_typing && is_selected { + if is_selected { Span::styled("▸ ", Style::default().fg(Theme::background()).add_modifier(Modifier::BOLD)) } else { Span::styled("▸ ", Style::default().fg(Theme::highlight())) @@ -142,23 +133,20 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) { Span::raw(" ") }; let name_spans = if in_search && !search_query.is_empty() { - highlight_search_matches(&item.node.name, &search_query, search_typing, is_selected) + highlight_search_matches(&item.node.name, &search_query, is_selected) } else { vec![Span::raw(&item.node.name)] }; let suffix = if item.node.is_dir { "/" } else { "" }; - let base_style = if search_typing { - // While typing search: no selection bar, just normal colors - if state.marked_files.contains(&item.node.path) { - Theme::marked() + let base_style = if is_selected { + // Selection bar: yellow/orange when in search (typing or viewing results), blue otherwise + if in_search { + Theme::search_selected() } else { - Theme::secondary() + Theme::selected() } - } else if is_selected { - // After pressing Enter or normal mode: normal blue selection bar - Theme::selected() } else if state.marked_files.contains(&item.node.path) { Theme::marked() } else { @@ -189,11 +177,8 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) { ); let mut list_state = ListState::default(); - // Don't show selection bar widget while typing search - we use inverted colors instead - // Show it in normal mode and after executing search (Enter) - if !search_typing { - list_state.select(Some(state.selected_index)); - } + // Always show selection bar (yellow/orange in search mode, blue otherwise) + list_state.select(Some(state.selected_index)); *list_state.offset_mut() = state.scroll_offset; frame.render_stateful_widget(list, area, &mut list_state); } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 3813f75..48a149e 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -91,4 +91,10 @@ impl Theme { .fg(Self::warning()) .bg(Self::background()) } + + pub fn search_selected() -> Style { + Style::default() + .fg(Self::background()) + .bg(Self::warning()) + } }