Improve search and playlist management

- Add folder priority boost in fuzzy search scoring
- Show search results in top status bar after Enter
- Change keybindings: v for mark, a for add to playlist, c for clear
- Add playlist management: add_to_playlist() and clear_playlist()
- Fix playlist index reset when clearing playlist
- Display incremental search status in bottom bar while typing
This commit is contained in:
2025-12-06 14:40:53 +01:00
parent e44c9e5bba
commit b535d0e9cb
4 changed files with 696 additions and 91 deletions

View File

@@ -7,7 +7,7 @@ mod ui;
use anyhow::{Context, Result};
use crossterm::{
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEventKind},
event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyEvent, KeyEventKind, KeyModifiers},
execute,
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
};
@@ -105,7 +105,7 @@ async fn run_app<B: ratatui::backend::Backend>(
if event::poll(std::time::Duration::from_millis(100))? {
if let Event::Key(key) = event::read()? {
if key.kind == KeyEventKind::Press {
handle_key_event(state, player, key.code).await?;
handle_key_event(state, player, key).await?;
}
}
}
@@ -118,51 +118,109 @@ async fn run_app<B: ratatui::backend::Backend>(
Ok(())
}
async fn handle_key_event(state: &mut AppState, player: &mut player::Player, key_code: KeyCode) -> Result<()> {
match key_code {
KeyCode::Char('q') => {
async fn handle_key_event(state: &mut AppState, player: &mut player::Player, key: KeyEvent) -> Result<()> {
// Handle search mode separately
if state.search_mode {
match key.code {
KeyCode::Char(c) => {
state.append_search_char(c);
}
KeyCode::Backspace => {
state.backspace_search();
}
KeyCode::Tab => {
state.tab_search_next();
}
KeyCode::BackTab => {
state.tab_search_prev();
}
KeyCode::Enter => {
state.execute_search();
}
KeyCode::Esc => {
state.exit_search_mode();
}
_ => {}
}
return Ok(());
}
match (key.code, key.modifiers) {
(KeyCode::Char('q'), _) => {
state.should_quit = true;
}
KeyCode::Char('k') => {
state.move_selection_up();
(KeyCode::Char('/'), _) => {
state.enter_search_mode();
}
KeyCode::Char('j') => {
state.move_selection_down();
(KeyCode::Esc, _) => {
state.search_matches.clear();
}
KeyCode::Char('h') => {
state.collapse_selected();
}
KeyCode::Char('l') => {
state.expand_selected();
}
KeyCode::Char('t') => {
state.toggle_mark();
}
KeyCode::Char('c') => {
state.clear_marks();
}
KeyCode::Char('n') => {
state.play_next();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Next track: {:?}", path);
(KeyCode::Char('n'), _) => {
if !state.search_matches.is_empty() {
state.next_search_match();
} else {
// If stopped, start from current index (0), otherwise go to next
if state.player_state == PlayerState::Stopped && !state.playlist.is_empty() {
state.current_file = Some(state.playlist[state.playlist_index].clone());
state.player_state = PlayerState::Playing;
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Starting playlist: {:?}", path);
}
} else {
state.play_next();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Next track: {:?}", path);
}
}
}
}
KeyCode::Char('p') => {
(KeyCode::Char('N'), KeyModifiers::SHIFT) => {
state.prev_search_match();
}
(KeyCode::Char('d'), KeyModifiers::CONTROL) => {
state.page_down();
}
(KeyCode::Char('u'), KeyModifiers::CONTROL) => {
state.page_up();
}
(KeyCode::Char('k'), _) | (KeyCode::Up, _) => {
state.move_selection_up();
}
(KeyCode::Char('j'), _) | (KeyCode::Down, _) => {
state.move_selection_down();
}
(KeyCode::Char('h'), _) => {
state.collapse_selected();
}
(KeyCode::Char('l'), _) => {
state.expand_selected();
}
(KeyCode::Char('v'), _) => {
state.toggle_mark();
}
(KeyCode::Char('a'), _) => {
state.add_to_playlist();
}
(KeyCode::Char('c'), _) => {
state.clear_playlist();
}
(KeyCode::Char('p'), _) => {
state.play_previous();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Previous track: {:?}", path);
}
}
KeyCode::Enter => {
(KeyCode::Enter, _) => {
state.play_selection();
if let Some(ref path) = state.current_file {
player.play(path)?;
tracing::info!("Playing: {:?} (playlist: {} tracks)", path, state.playlist.len());
}
}
KeyCode::Char(' ') => {
(KeyCode::Char(' '), _) => {
match state.player_state {
PlayerState::Playing => {
player.pause()?;
@@ -177,31 +235,31 @@ async fn handle_key_event(state: &mut AppState, player: &mut player::Player, key
PlayerState::Stopped => {}
}
}
KeyCode::Left => {
(KeyCode::Left, _) => {
if state.player_state != PlayerState::Stopped {
player.seek(-10.0)?;
tracing::info!("Seek backward 10s");
}
}
KeyCode::Right => {
(KeyCode::Right, _) => {
if state.player_state != PlayerState::Stopped {
player.seek(10.0)?;
tracing::info!("Seek forward 10s");
}
}
KeyCode::Char('+') | KeyCode::Char('=') => {
(KeyCode::Char('+'), _) | (KeyCode::Char('='), _) => {
let new_volume = (state.volume + 5).min(100);
state.volume = new_volume;
player.set_volume(new_volume)?;
tracing::info!("Volume: {}%", new_volume);
}
KeyCode::Char('-') => {
(KeyCode::Char('-'), _) => {
let new_volume = (state.volume - 5).max(0);
state.volume = new_volume;
player.set_volume(new_volume)?;
tracing::info!("Volume: {}%", new_volume);
}
KeyCode::Char('r') => {
(KeyCode::Char('r'), _) => {
state.is_refreshing = true;
tracing::info!("Rescanning...");
let cache_dir = cache::get_cache_dir()?;