Improve playlist handling and add UI enhancements
All checks were successful
Build and Release / build-and-release (push) Successful in 1m19s
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:
720
src/state/mod.rs
720
src/state/mod.rs
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user