Add playlist bounds validation and search mode visual indicator
All checks were successful
Build and Release / build-and-release (push) Successful in 1m1s
All checks were successful
Build and Release / build-and-release (push) Successful in 1m1s
- Add bounds checking to prevent accessing invalid playlist indices - Yellow/orange selection bar when in search mode - Validate playlist index after navigation operations - Handle empty playlists gracefully
This commit is contained in:
parent
248c5701fb
commit
59f9f548c1
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-player"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
86
src/main.rs
86
src/main.rs
@ -339,29 +339,32 @@ async fn handle_key_event<B: ratatui::backend::Backend>(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<B: ratatui::backend::Backend>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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<Span<'a>> {
|
||||
fn highlight_search_matches<'a>(text: &str, query: &str, is_selected: bool) -> Vec<Span<'a>> {
|
||||
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<ListItem> = 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);
|
||||
}
|
||||
|
||||
@ -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())
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user