Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4529fad61d |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-player"
|
name = "cm-player"
|
||||||
version = "0.1.23"
|
version = "0.1.24"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -97,8 +97,8 @@ fn main() -> Result<()> {
|
|||||||
|
|
||||||
fn action_toggle_folder(state: &mut AppState) {
|
fn action_toggle_folder(state: &mut AppState) {
|
||||||
if let Some(item) = state.get_selected_item() {
|
if let Some(item) = state.get_selected_item() {
|
||||||
if item.node.is_dir {
|
if item.is_dir {
|
||||||
let path = item.node.path.clone();
|
let path = item.path.clone();
|
||||||
if state.expanded_dirs.contains(&path) {
|
if state.expanded_dirs.contains(&path) {
|
||||||
// Folder is open, close it
|
// Folder is open, close it
|
||||||
state.collapse_selected();
|
state.collapse_selected();
|
||||||
@@ -936,7 +936,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
|||||||
if is_double_click {
|
if is_double_click {
|
||||||
// Double click = toggle folder or play file
|
// Double click = toggle folder or play file
|
||||||
if let Some(item) = state.get_selected_item() {
|
if let Some(item) = state.get_selected_item() {
|
||||||
if item.node.is_dir {
|
if item.is_dir {
|
||||||
action_toggle_folder(state);
|
action_toggle_folder(state);
|
||||||
} else {
|
} else {
|
||||||
action_play_selection(state, player)?;
|
action_play_selection(state, player)?;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use crate::cache::{Cache, FileTreeNode};
|
use crate::cache::{Cache, FileTreeNode};
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::path::PathBuf;
|
use std::path::{Path, PathBuf};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
// Fuzzy match scoring bonuses
|
// Fuzzy match scoring bonuses
|
||||||
@@ -96,7 +96,9 @@ pub struct AppState {
|
|||||||
|
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct FlattenedItem {
|
pub struct FlattenedItem {
|
||||||
pub node: FileTreeNode,
|
pub path: PathBuf,
|
||||||
|
pub name: String,
|
||||||
|
pub is_dir: bool,
|
||||||
pub depth: usize,
|
pub depth: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -336,11 +338,29 @@ impl AppState {
|
|||||||
self.flattened_items.get(self.selected_index)
|
self.flattened_items.get(self.selected_index)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper to find a node in the tree by path
|
||||||
|
fn find_node_by_path<'a>(&'a self, path: &Path) -> Option<&'a FileTreeNode> {
|
||||||
|
fn search_nodes<'a>(nodes: &'a [FileTreeNode], path: &Path) -> Option<&'a FileTreeNode> {
|
||||||
|
for node in nodes {
|
||||||
|
if node.path == path {
|
||||||
|
return Some(node);
|
||||||
|
}
|
||||||
|
if node.is_dir {
|
||||||
|
if let Some(found) = search_nodes(&node.children, path) {
|
||||||
|
return Some(found);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
search_nodes(&self.cache.file_tree, path)
|
||||||
|
}
|
||||||
|
|
||||||
pub fn collapse_selected(&mut self) {
|
pub fn collapse_selected(&mut self) {
|
||||||
let item = self.get_selected_item().cloned();
|
let item = self.get_selected_item().cloned();
|
||||||
if let Some(item) = item {
|
if let Some(item) = item {
|
||||||
if item.node.is_dir {
|
if item.is_dir {
|
||||||
let path = item.node.path.clone();
|
let path = item.path.clone();
|
||||||
let was_expanded = self.expanded_dirs.contains(&path);
|
let was_expanded = self.expanded_dirs.contains(&path);
|
||||||
|
|
||||||
if was_expanded {
|
if was_expanded {
|
||||||
@@ -348,7 +368,7 @@ impl AppState {
|
|||||||
self.expanded_dirs.remove(&path);
|
self.expanded_dirs.remove(&path);
|
||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
// Find the collapsed folder and select it
|
// Find the collapsed folder and select it
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|i| i.node.path == path) {
|
if let Some(idx) = self.flattened_items.iter().position(|i| i.path == path) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -358,19 +378,19 @@ impl AppState {
|
|||||||
self.expanded_dirs.remove(&parent_buf);
|
self.expanded_dirs.remove(&parent_buf);
|
||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
// Jump to parent folder
|
// Jump to parent folder
|
||||||
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.node.path == parent_buf) {
|
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.path == parent_buf) {
|
||||||
self.selected_index = parent_idx;
|
self.selected_index = parent_idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Close parent folder when on a file and jump to it
|
// Close parent folder when on a file and jump to it
|
||||||
if let Some(parent) = item.node.path.parent() {
|
if let Some(parent) = item.path.parent() {
|
||||||
let parent_buf = parent.to_path_buf();
|
let parent_buf = parent.to_path_buf();
|
||||||
self.expanded_dirs.remove(&parent_buf);
|
self.expanded_dirs.remove(&parent_buf);
|
||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
// Jump to parent folder
|
// Jump to parent folder
|
||||||
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.node.path == parent_buf) {
|
if let Some(parent_idx) = self.flattened_items.iter().position(|i| i.path == parent_buf) {
|
||||||
self.selected_index = parent_idx;
|
self.selected_index = parent_idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -380,8 +400,8 @@ impl AppState {
|
|||||||
|
|
||||||
pub fn expand_selected(&mut self) {
|
pub fn expand_selected(&mut self) {
|
||||||
if let Some(item) = self.get_selected_item() {
|
if let Some(item) = self.get_selected_item() {
|
||||||
if item.node.is_dir {
|
if item.is_dir {
|
||||||
let path = item.node.path.clone();
|
let path = item.path.clone();
|
||||||
self.expanded_dirs.insert(path);
|
self.expanded_dirs.insert(path);
|
||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
}
|
}
|
||||||
@@ -401,8 +421,8 @@ impl AppState {
|
|||||||
self.marked_files.clear();
|
self.marked_files.clear();
|
||||||
// Mark current file
|
// Mark current file
|
||||||
if let Some(item) = self.get_selected_item() {
|
if let Some(item) = self.get_selected_item() {
|
||||||
if !item.node.is_dir {
|
if !item.is_dir {
|
||||||
self.marked_files.insert(item.node.path.clone());
|
self.marked_files.insert(item.path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -418,8 +438,8 @@ impl AppState {
|
|||||||
|
|
||||||
for i in start..=end {
|
for i in start..=end {
|
||||||
if let Some(item) = self.flattened_items.get(i) {
|
if let Some(item) = self.flattened_items.get(i) {
|
||||||
if !item.node.is_dir {
|
if !item.is_dir {
|
||||||
self.marked_files.insert(item.node.path.clone());
|
self.marked_files.insert(item.path.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -439,15 +459,19 @@ impl AppState {
|
|||||||
files.sort();
|
files.sort();
|
||||||
self.playlist.extend(files);
|
self.playlist.extend(files);
|
||||||
} else if let Some(item) = self.get_selected_item() {
|
} else if let Some(item) = self.get_selected_item() {
|
||||||
let node = item.node.clone();
|
let path = item.path.clone();
|
||||||
if node.is_dir {
|
let is_dir = item.is_dir;
|
||||||
|
if is_dir {
|
||||||
|
// Look up the full node to get children
|
||||||
|
if let Some(node) = self.find_node_by_path(&path) {
|
||||||
// Add all files in directory (allow duplicates)
|
// Add all files in directory (allow duplicates)
|
||||||
let mut files = collect_files_from_node(&node);
|
let mut files = collect_files_from_node(node);
|
||||||
files.sort();
|
files.sort();
|
||||||
self.playlist.extend(files);
|
self.playlist.extend(files);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Add single file (allow duplicates)
|
// Add single file (allow duplicates)
|
||||||
self.playlist.push(node.path.clone());
|
self.playlist.push(path);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -468,10 +492,12 @@ impl AppState {
|
|||||||
self.current_file = None;
|
self.current_file = None;
|
||||||
}
|
}
|
||||||
} else if let Some(item) = self.get_selected_item() {
|
} else if let Some(item) = self.get_selected_item() {
|
||||||
let node = item.node.clone();
|
let path = item.path.clone();
|
||||||
if node.is_dir {
|
let is_dir = item.is_dir;
|
||||||
// Play all files in directory
|
if is_dir {
|
||||||
self.playlist = collect_files_from_node(&node);
|
// Play all files in directory - look up node to get children
|
||||||
|
if let Some(node) = self.find_node_by_path(&path) {
|
||||||
|
self.playlist = collect_files_from_node(node);
|
||||||
self.playlist_index = 0;
|
self.playlist_index = 0;
|
||||||
self.playlist_scroll_offset = 0;
|
self.playlist_scroll_offset = 0;
|
||||||
self.selected_playlist_index = 0;
|
self.selected_playlist_index = 0;
|
||||||
@@ -481,9 +507,9 @@ impl AppState {
|
|||||||
// Empty directory
|
// Empty directory
|
||||||
self.current_file = None;
|
self.current_file = None;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Play single file
|
// Play single file
|
||||||
let path = node.path.clone();
|
|
||||||
self.playlist = vec![path.clone()];
|
self.playlist = vec![path.clone()];
|
||||||
self.playlist_index = 0;
|
self.playlist_index = 0;
|
||||||
self.playlist_scroll_offset = 0;
|
self.playlist_scroll_offset = 0;
|
||||||
@@ -674,7 +700,7 @@ impl AppState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the best match in the flattened list and jump to it
|
// 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) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == best_match) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
|
|
||||||
// Scroll to show the match
|
// Scroll to show the match
|
||||||
@@ -741,7 +767,7 @@ impl AppState {
|
|||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
|
|
||||||
// Find first match in flattened list
|
// Find first match in flattened list
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == first_match) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == first_match) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -766,7 +792,7 @@ impl AppState {
|
|||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
|
|
||||||
// Find the path in current flattened items
|
// Find the path in current flattened items
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == target_path) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == target_path) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
|
|
||||||
// Scroll to show the match
|
// Scroll to show the match
|
||||||
@@ -813,7 +839,7 @@ impl AppState {
|
|||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
|
|
||||||
// Find the path in current flattened items
|
// Find the path in current flattened items
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == target_path) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == target_path) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
|
|
||||||
// Scroll to show the match
|
// Scroll to show the match
|
||||||
@@ -860,7 +886,7 @@ impl AppState {
|
|||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
|
|
||||||
// Find and select the match
|
// Find and select the match
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == next_match) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == next_match) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
|
|
||||||
// Scroll to show the match
|
// Scroll to show the match
|
||||||
@@ -903,7 +929,7 @@ impl AppState {
|
|||||||
self.rebuild_flattened_items();
|
self.rebuild_flattened_items();
|
||||||
|
|
||||||
// Find and select the match
|
// Find and select the match
|
||||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.node.path == prev_match) {
|
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == prev_match) {
|
||||||
self.selected_index = idx;
|
self.selected_index = idx;
|
||||||
|
|
||||||
// Scroll to show the match
|
// Scroll to show the match
|
||||||
@@ -1160,7 +1186,9 @@ fn flatten_tree(nodes: &[FileTreeNode], depth: usize, expanded_dirs: &HashSet<Pa
|
|||||||
let is_expanded = expanded_dirs.contains(&node.path);
|
let is_expanded = expanded_dirs.contains(&node.path);
|
||||||
|
|
||||||
result.push(FlattenedItem {
|
result.push(FlattenedItem {
|
||||||
node: node.clone(),
|
path: node.path.clone(),
|
||||||
|
name: node.name.clone(),
|
||||||
|
is_dir: node.is_dir,
|
||||||
depth,
|
depth,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -142,15 +142,15 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
|||||||
.map(|(display_idx, item)| {
|
.map(|(display_idx, item)| {
|
||||||
let idx = state.scroll_offset + display_idx;
|
let idx = state.scroll_offset + display_idx;
|
||||||
let indent = " ".repeat(item.depth);
|
let indent = " ".repeat(item.depth);
|
||||||
let mark = if state.marked_files.contains(&item.node.path) { "* " } else { "" };
|
let mark = if state.marked_files.contains(&item.path) { "* " } else { "" };
|
||||||
|
|
||||||
// Build name with search highlighting
|
// Build name with search highlighting
|
||||||
// Only show selection bar when file panel has focus
|
// Only show selection bar when file panel has focus
|
||||||
let is_selected = !state.focus_playlist && idx == state.selected_index;
|
let is_selected = !state.focus_playlist && idx == state.selected_index;
|
||||||
|
|
||||||
// Add icon for directories and files
|
// Add icon for directories and files
|
||||||
let icon = if item.node.is_dir {
|
let icon = if item.is_dir {
|
||||||
let is_expanded = state.expanded_dirs.contains(&item.node.path);
|
let is_expanded = state.expanded_dirs.contains(&item.path);
|
||||||
// Nerd font folder icons: \u{eaf7} = open, \u{ea83} = closed
|
// Nerd font folder icons: \u{eaf7} = open, \u{ea83} = closed
|
||||||
let icon_char = if is_expanded { "\u{eaf7} " } else { "\u{ea83} " };
|
let icon_char = if is_expanded { "\u{eaf7} " } else { "\u{ea83} " };
|
||||||
|
|
||||||
@@ -162,7 +162,7 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// File icons based on extension
|
// File icons based on extension
|
||||||
let extension = item.node.path.extension()
|
let extension = item.path.extension()
|
||||||
.and_then(|e| e.to_str())
|
.and_then(|e| e.to_str())
|
||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.to_lowercase();
|
.to_lowercase();
|
||||||
@@ -184,12 +184,12 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let name_spans = if in_search && !search_query.is_empty() {
|
let name_spans = if in_search && !search_query.is_empty() {
|
||||||
highlight_search_matches(&item.node.name, &search_query, is_selected)
|
highlight_search_matches(&item.name, &search_query, is_selected)
|
||||||
} else {
|
} else {
|
||||||
vec![Span::raw(&item.node.name)]
|
vec![Span::raw(&item.name)]
|
||||||
};
|
};
|
||||||
|
|
||||||
let suffix = if item.node.is_dir { "/" } else { "" };
|
let suffix = if item.is_dir { "/" } else { "" };
|
||||||
|
|
||||||
let base_style = if is_selected {
|
let base_style = if is_selected {
|
||||||
// Selection bar: yellow/orange when in search (typing or viewing results), blue otherwise
|
// Selection bar: yellow/orange when in search (typing or viewing results), blue otherwise
|
||||||
@@ -198,7 +198,7 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
|||||||
} else {
|
} else {
|
||||||
Theme::selected()
|
Theme::selected()
|
||||||
}
|
}
|
||||||
} else if state.marked_files.contains(&item.node.path) {
|
} else if state.marked_files.contains(&item.path) {
|
||||||
Theme::marked()
|
Theme::marked()
|
||||||
} else {
|
} else {
|
||||||
Theme::secondary()
|
Theme::secondary()
|
||||||
|
|||||||
Reference in New Issue
Block a user