Improve playlist handling and add UI enhancements
All checks were successful
Build and Release / build-and-release (push) Successful in 1m19s

- Fix cascading track failures when files can't be loaded
- Add position check (>0.5s) to prevent auto-advance on failed tracks
- Fix playlist scroll jumping when browsing while tracks auto-advance
- Only auto-scroll playlist when not focused on playlist panel
- Fix selector bar getting hidden by "... X more below" indicator
- Add effective height calculation for all navigation functions
- Fix Ctrl-U/D page scrolling in both panels

- Add play mode system (Normal/Loop) with 'm' key toggle
- Add Shift+R to randomize playlist order
- Add right-click context menus:
  - Playlist: Remove, Randomise
  - Title bar: Stop, Loop, Refresh
- Make context menus more compact (13 chars wide)
- Show play mode indicator in title bar ([Loop])

- Add incremental search support in playlist panel
- Fix search scrolling to account for bottom indicator
- Show search query in status bar when navigating results
- Change search text to white color

- Improve double-click detection and single-click selection
- Add focus indicators (bold title for active panel)
- Hide selector bar in inactive panel
This commit is contained in:
2025-12-08 19:53:32 +01:00
parent 59f9f548c1
commit f60ff02b2a
4 changed files with 1472 additions and 116 deletions

View File

@@ -2,6 +2,7 @@ use crate::cache::{Cache, FileTreeNode};
use crate::config::Config;
use std::collections::HashSet;
use std::path::PathBuf;
use std::time::Instant;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlayerState {
@@ -10,11 +11,34 @@ pub enum PlayerState {
Paused,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlayMode {
Normal, // Play through once
Loop, // Repeat playlist
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ContextMenu {
pub menu_type: ContextMenuType,
pub x: u16,
pub y: u16,
pub selected_index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContextMenuType {
FilePanel, // Shows "Play" and "Add"
Playlist, // Shows "Remove" and "Randomise"
TitleBar, // Shows "Stop" and "Loop"
}
pub struct AppState {
pub cache: Cache,
pub config: Config,
pub selected_index: usize,
pub scroll_offset: usize,
pub file_panel_visible_height: usize,
pub playlist_visible_height: usize,
pub player_state: PlayerState,
pub current_file: Option<PathBuf>,
pub current_position: f64,
@@ -26,6 +50,8 @@ pub struct AppState {
pub marked_files: HashSet<PathBuf>,
pub playlist: Vec<PathBuf>,
pub playlist_index: usize,
pub playlist_scroll_offset: usize,
pub selected_playlist_index: usize,
pub is_refreshing: bool,
pub search_mode: bool,
pub search_query: String,
@@ -33,10 +59,20 @@ pub struct AppState {
pub search_match_index: usize,
pub tab_search_results: Vec<PathBuf>,
pub tab_search_index: usize,
pub playlist_search_matches: Vec<usize>,
pub playlist_search_match_index: usize,
pub playlist_tab_search_results: Vec<usize>,
pub playlist_tab_search_index: usize,
pub visual_mode: bool,
pub visual_anchor: usize,
pub saved_expanded_dirs: HashSet<PathBuf>,
pub show_refresh_confirm: bool,
pub focus_playlist: bool,
pub last_click_time: Option<Instant>,
pub last_click_index: Option<usize>,
pub last_click_is_playlist: bool,
pub context_menu: Option<ContextMenu>,
pub play_mode: PlayMode,
}
#[derive(Debug, Clone)]
@@ -57,6 +93,8 @@ impl AppState {
config,
selected_index: 0,
scroll_offset: 0,
file_panel_visible_height: 20,
playlist_visible_height: 20,
player_state: PlayerState::Stopped,
current_file: None,
current_position: 0.0,
@@ -68,6 +106,8 @@ impl AppState {
marked_files: HashSet::new(),
playlist: Vec::new(),
playlist_index: 0,
playlist_scroll_offset: 0,
selected_playlist_index: 0,
is_refreshing: false,
search_mode: false,
search_query: String::new(),
@@ -75,10 +115,20 @@ impl AppState {
search_match_index: 0,
tab_search_results: Vec::new(),
tab_search_index: 0,
playlist_search_matches: Vec::new(),
playlist_search_match_index: 0,
playlist_tab_search_results: Vec::new(),
playlist_tab_search_index: 0,
visual_mode: false,
visual_anchor: 0,
saved_expanded_dirs: HashSet::new(),
show_refresh_confirm: false,
focus_playlist: false,
last_click_time: None,
last_click_index: None,
last_click_is_playlist: false,
context_menu: None,
play_mode: PlayMode::Normal,
}
}
@@ -99,6 +149,24 @@ impl AppState {
pub fn move_selection_down(&mut self) {
if self.selected_index < self.flattened_items.len().saturating_sub(1) {
self.selected_index += 1;
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
// Scroll down when selection reaches bottom
if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
// Update visual selection if in visual mode
if self.visual_mode {
self.update_visual_selection();
@@ -106,29 +174,172 @@ impl AppState {
}
}
pub fn update_scroll_offset(&mut self, visible_height: usize) {
// Scroll down when selection reaches bottom
if self.selected_index >= self.scroll_offset + visible_height {
self.scroll_offset = self.selected_index - visible_height + 1;
pub fn scroll_view_up(&mut self) {
// Scroll view up without changing selection
if self.scroll_offset > 0 {
self.scroll_offset -= 1;
}
// Scroll up when selection reaches top
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
}
pub fn scroll_view_down(&mut self, visible_height: usize) {
// Scroll view down without changing selection
let max_scroll = self.flattened_items.len().saturating_sub(visible_height);
if self.scroll_offset < max_scroll {
self.scroll_offset += 1;
}
}
pub fn scroll_playlist_up(&mut self) {
// Scroll playlist view up
if self.playlist_scroll_offset > 0 {
self.playlist_scroll_offset -= 1;
}
}
pub fn scroll_playlist_down(&mut self, visible_height: usize) {
// Scroll playlist view down
let max_scroll = self.playlist.len().saturating_sub(visible_height);
if self.playlist_scroll_offset < max_scroll {
self.playlist_scroll_offset += 1;
}
}
pub fn update_playlist_scroll(&mut self, visible_height: usize) {
// Auto-scroll playlist to keep current track visible
if self.playlist_index >= self.playlist_scroll_offset + visible_height {
// Track is below visible area, scroll down
self.playlist_scroll_offset = self.playlist_index - visible_height + 1;
} else if self.playlist_index < self.playlist_scroll_offset {
// Track is above visible area, scroll up
self.playlist_scroll_offset = self.playlist_index;
}
}
pub fn move_playlist_selection_up(&mut self) {
if self.selected_playlist_index > 0 {
self.selected_playlist_index -= 1;
// Scroll up when selection reaches top
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
}
}
}
pub fn move_playlist_selection_down(&mut self, visible_height: usize) {
if self.selected_playlist_index < self.playlist.len().saturating_sub(1) {
self.selected_playlist_index += 1;
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
visible_height.saturating_sub(1)
} else {
visible_height
};
// Scroll down when selection reaches bottom
if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
}
pub fn remove_selected_playlist_item(&mut self) {
if self.selected_playlist_index < self.playlist.len() {
self.playlist.remove(self.selected_playlist_index);
// Adjust playlist_index if necessary
if self.playlist_index > self.selected_playlist_index {
self.playlist_index -= 1;
} else if self.playlist_index == self.selected_playlist_index {
// Keep same index (which is now the next track)
if self.playlist_index >= self.playlist.len() && !self.playlist.is_empty() {
self.playlist_index = self.playlist.len() - 1;
}
}
// Adjust selected_playlist_index if at end
if self.selected_playlist_index >= self.playlist.len() && !self.playlist.is_empty() {
self.selected_playlist_index = self.playlist.len() - 1;
}
}
}
pub fn playlist_page_down(&mut self) {
// Move down by half page (vim Ctrl-D behavior)
let half_page = self.playlist_visible_height / 2;
let new_index = (self.selected_playlist_index + half_page).min(self.playlist.len().saturating_sub(1));
self.selected_playlist_index = new_index;
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
// Adjust scroll if needed
if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
pub fn playlist_page_up(&mut self) {
// Move up by half page (vim Ctrl-U behavior)
let half_page = self.playlist_visible_height / 2;
let new_index = self.selected_playlist_index.saturating_sub(half_page);
self.selected_playlist_index = new_index;
// Adjust scroll if needed
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
}
}
pub fn page_down(&mut self) {
// Move down by half page (vim Ctrl-D behavior)
let half_page = 10; // Default half page size
let half_page = self.file_panel_visible_height / 2;
let new_index = (self.selected_index + half_page).min(self.flattened_items.len().saturating_sub(1));
self.selected_index = new_index;
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
// Adjust scroll if needed
if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
pub fn page_up(&mut self) {
// Move up by half page (vim Ctrl-U behavior)
let half_page = 10; // Default half page size
let half_page = self.file_panel_visible_height / 2;
let new_index = self.selected_index.saturating_sub(half_page);
self.selected_index = new_index;
// Adjust scroll if needed
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
}
}
pub fn get_selected_item(&self) -> Option<&FlattenedItem> {
@@ -234,29 +445,20 @@ impl AppState {
pub fn add_to_playlist(&mut self) {
// Add marked files or current selection to playlist
if !self.marked_files.is_empty() {
// Add marked files
for path in &self.marked_files {
if !self.playlist.contains(path) {
self.playlist.push(path.clone());
}
}
self.playlist.sort();
// Add marked files (allow duplicates)
let mut files: Vec<PathBuf> = self.marked_files.iter().cloned().collect();
files.sort();
self.playlist.extend(files);
} else if let Some(item) = self.get_selected_item() {
let node = item.node.clone();
if node.is_dir {
// Add all files in directory
let files = collect_files_from_node(&node);
for path in files {
if !self.playlist.contains(&path) {
self.playlist.push(path);
}
}
self.playlist.sort();
// Add all files in directory (allow duplicates)
let mut files = collect_files_from_node(&node);
files.sort();
self.playlist.extend(files);
} else {
// Add single file
if !self.playlist.contains(&node.path) {
self.playlist.push(node.path.clone());
}
// Add single file (allow duplicates)
self.playlist.push(node.path.clone());
}
}
}
@@ -268,6 +470,8 @@ impl AppState {
self.playlist = self.marked_files.iter().cloned().collect();
self.playlist.sort();
self.playlist_index = 0;
self.playlist_scroll_offset = 0;
self.selected_playlist_index = 0;
if let Some(first) = self.playlist.first() {
self.current_file = Some(first.clone());
self.player_state = PlayerState::Playing;
@@ -282,6 +486,8 @@ impl AppState {
// Play all files in directory
self.playlist = collect_files_from_node(&node);
self.playlist_index = 0;
self.playlist_scroll_offset = 0;
self.selected_playlist_index = 0;
if let Some(first) = self.playlist.first() {
self.current_file = Some(first.clone());
self.player_state = PlayerState::Playing;
@@ -295,6 +501,8 @@ impl AppState {
let path = node.path.clone();
self.playlist = vec![path.clone()];
self.playlist_index = 0;
self.playlist_scroll_offset = 0;
self.selected_playlist_index = 0;
self.current_file = Some(path);
self.player_state = PlayerState::Playing;
}
@@ -302,16 +510,73 @@ impl AppState {
}
pub fn play_next(&mut self) {
if self.playlist_index + 1 < self.playlist.len() {
self.playlist_index += 1;
// Double-check index is valid before accessing
if self.playlist_index < self.playlist.len() {
if self.playlist.is_empty() {
return;
}
match self.play_mode {
PlayMode::Normal => {
// Play through once, stop at end
if self.playlist_index + 1 < self.playlist.len() {
self.playlist_index += 1;
if self.playlist_index < self.playlist.len() {
self.current_file = Some(self.playlist[self.playlist_index].clone());
self.player_state = PlayerState::Playing;
}
} else {
// Reached end, stop
self.player_state = PlayerState::Stopped;
}
}
PlayMode::Loop => {
// Loop back to beginning when reaching end
self.playlist_index = (self.playlist_index + 1) % self.playlist.len();
self.current_file = Some(self.playlist[self.playlist_index].clone());
self.player_state = PlayerState::Playing;
}
}
}
pub fn cycle_play_mode(&mut self) {
self.play_mode = match self.play_mode {
PlayMode::Normal => PlayMode::Loop,
PlayMode::Loop => PlayMode::Normal,
};
}
pub fn shuffle_playlist(&mut self) {
if self.playlist.is_empty() {
return;
}
use rand::seq::SliceRandom;
let mut rng = rand::thread_rng();
// Remember the currently playing track
let current_track = if self.playlist_index < self.playlist.len() {
Some(self.playlist[self.playlist_index].clone())
} else {
None
};
// Shuffle the playlist
self.playlist.shuffle(&mut rng);
// Find the new position of the currently playing track
if let Some(track) = current_track {
if let Some(new_index) = self.playlist.iter().position(|p| p == &track) {
self.playlist_index = new_index;
self.selected_playlist_index = new_index;
} else {
self.playlist_index = 0;
self.selected_playlist_index = 0;
}
} else {
self.playlist_index = 0;
self.selected_playlist_index = 0;
}
}
pub fn refresh_flattened_items(&mut self) {
// Keep current expanded state after rescan
self.rebuild_flattened_items();
@@ -327,21 +592,39 @@ impl AppState {
pub fn enter_search_mode(&mut self) {
self.search_mode = true;
self.search_query.clear();
self.search_matches.clear();
self.search_match_index = 0;
self.tab_search_results.clear();
self.tab_search_index = 0;
// Save current folder state
self.saved_expanded_dirs = self.expanded_dirs.clone();
if self.focus_playlist {
// Clear playlist search state
self.playlist_search_matches.clear();
self.playlist_search_match_index = 0;
self.playlist_tab_search_results.clear();
self.playlist_tab_search_index = 0;
} else {
// Clear file search state
self.search_matches.clear();
self.search_match_index = 0;
self.tab_search_results.clear();
self.tab_search_index = 0;
// Save current folder state
self.saved_expanded_dirs = self.expanded_dirs.clone();
}
}
pub fn exit_search_mode(&mut self) {
self.search_mode = false;
self.tab_search_results.clear();
self.tab_search_index = 0;
// Restore folder state from before search
self.expanded_dirs = self.saved_expanded_dirs.clone();
self.rebuild_flattened_items();
if self.focus_playlist {
// Clear playlist search state
self.playlist_tab_search_results.clear();
self.playlist_tab_search_index = 0;
} else {
// Clear file search state
self.tab_search_results.clear();
self.tab_search_index = 0;
// Restore folder state from before search
self.expanded_dirs = self.saved_expanded_dirs.clone();
self.rebuild_flattened_items();
}
}
pub fn append_search_char(&mut self, c: char) {
@@ -400,6 +683,26 @@ impl AppState {
// Find the best match in the flattened list and jump to it
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == best_match) {
self.selected_index = idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
} else if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
}
@@ -479,6 +782,26 @@ impl AppState {
// Find the path in current flattened items
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == target_path) {
self.selected_index = idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
} else if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
}
}
@@ -506,6 +829,26 @@ impl AppState {
// Find the path in current flattened items
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == target_path) {
self.selected_index = idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
} else if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
}
}
@@ -533,6 +876,26 @@ impl AppState {
// Find and select the match
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == next_match) {
self.selected_index = idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
} else if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
}
@@ -563,6 +926,277 @@ impl AppState {
// Find and select the match
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == prev_match) {
self.selected_index = idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.scroll_offset + self.file_panel_visible_height;
let items_below = if visible_end < self.flattened_items.len() {
self.flattened_items.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.file_panel_visible_height.saturating_sub(1)
} else {
self.file_panel_visible_height
};
if self.selected_index < self.scroll_offset {
self.scroll_offset = self.selected_index;
} else if self.selected_index >= self.scroll_offset + effective_height {
self.scroll_offset = self.selected_index - effective_height + 1;
}
}
}
pub fn append_playlist_search_char(&mut self, c: char) {
self.search_query.push(c);
self.perform_playlist_incremental_search();
}
pub fn backspace_playlist_search(&mut self) {
self.search_query.pop();
self.perform_playlist_incremental_search();
}
fn perform_playlist_incremental_search(&mut self) {
if self.search_query.is_empty() {
self.playlist_tab_search_results.clear();
self.playlist_tab_search_index = 0;
return;
}
// Collect all matching indices with scores
let mut matching_indices_with_scores: Vec<(usize, i32)> = self.playlist
.iter()
.enumerate()
.filter_map(|(idx, path)| {
let filename = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
fuzzy_match(filename, &self.search_query).map(|score| (idx, score))
})
.collect();
if matching_indices_with_scores.is_empty() {
self.playlist_tab_search_results.clear();
self.playlist_tab_search_index = 0;
return;
}
// Sort by score (highest first)
matching_indices_with_scores.sort_by(|a, b| b.1.cmp(&a.1));
// Store all matches for tab completion
self.playlist_tab_search_results = matching_indices_with_scores.iter().map(|(idx, _)| *idx).collect();
self.playlist_tab_search_index = 0;
// Jump to best match
let best_match_idx = self.playlist_tab_search_results[0];
self.selected_playlist_index = best_match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
pub fn playlist_tab_search_next(&mut self) {
if self.playlist_tab_search_results.is_empty() {
return;
}
// Cycle to next match
self.playlist_tab_search_index = (self.playlist_tab_search_index + 1) % self.playlist_tab_search_results.len();
let next_match_idx = self.playlist_tab_search_results[self.playlist_tab_search_index];
self.selected_playlist_index = next_match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
pub fn playlist_tab_search_prev(&mut self) {
if self.playlist_tab_search_results.is_empty() {
return;
}
// Cycle to previous match
if self.playlist_tab_search_index == 0 {
self.playlist_tab_search_index = self.playlist_tab_search_results.len() - 1;
} else {
self.playlist_tab_search_index -= 1;
}
let prev_match_idx = self.playlist_tab_search_results[self.playlist_tab_search_index];
self.selected_playlist_index = prev_match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
pub fn execute_playlist_search(&mut self) {
if self.search_query.is_empty() {
self.search_mode = false;
return;
}
// Collect all matching indices with scores
let mut matching_indices_with_scores: Vec<(usize, i32)> = self.playlist
.iter()
.enumerate()
.filter_map(|(idx, path)| {
let filename = path.file_name()
.and_then(|n| n.to_str())
.unwrap_or("");
fuzzy_match(filename, &self.search_query).map(|score| (idx, score))
})
.collect();
if matching_indices_with_scores.is_empty() {
self.search_mode = false;
return;
}
// Sort by score (highest first)
matching_indices_with_scores.sort_by(|a, b| b.1.cmp(&a.1));
// Store matching indices
self.playlist_search_matches = matching_indices_with_scores.iter().map(|(idx, _)| *idx).collect();
if !self.playlist_search_matches.is_empty() {
self.playlist_search_match_index = 0;
let first_match_idx = self.playlist_search_matches[0];
self.selected_playlist_index = first_match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
self.search_mode = false;
}
pub fn next_playlist_search_match(&mut self) {
if !self.playlist_search_matches.is_empty() {
self.playlist_search_match_index = (self.playlist_search_match_index + 1) % self.playlist_search_matches.len();
let match_idx = self.playlist_search_matches[self.playlist_search_match_index];
self.selected_playlist_index = match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
}
pub fn prev_playlist_search_match(&mut self) {
if !self.playlist_search_matches.is_empty() {
if self.playlist_search_match_index == 0 {
self.playlist_search_match_index = self.playlist_search_matches.len() - 1;
} else {
self.playlist_search_match_index -= 1;
}
let match_idx = self.playlist_search_matches[self.playlist_search_match_index];
self.selected_playlist_index = match_idx;
// Scroll to show the match
// Account for "... X more below" indicator which takes one line
let visible_end = self.playlist_scroll_offset + self.playlist_visible_height;
let items_below = if visible_end < self.playlist.len() {
self.playlist.len() - visible_end
} else {
0
};
let effective_height = if items_below > 0 {
self.playlist_visible_height.saturating_sub(1)
} else {
self.playlist_visible_height
};
if self.selected_playlist_index < self.playlist_scroll_offset {
self.playlist_scroll_offset = self.selected_playlist_index;
} else if self.selected_playlist_index >= self.playlist_scroll_offset + effective_height {
self.playlist_scroll_offset = self.selected_playlist_index - effective_height + 1;
}
}
}