Reduce memory usage by ~50 MB for large libraries
All checks were successful
Build and Release / build-and-release (push) Successful in 50s
All checks were successful
Build and Release / build-and-release (push) Successful in 50s
FlattenedItem now stores only essential fields (path, name, is_dir, depth) instead of cloning entire FileTreeNode structures. For 500,000 files, this reduces memory from ~100 MB to ~50 MB for the flattened view. - Extract only needed fields in flatten_tree() - Add find_node_by_path() helper to look up full nodes when needed - Update all UI and state code to use new structure
This commit is contained in:
parent
6ad522f27c
commit
4529fad61d
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "cm-player"
|
||||
version = "0.1.23"
|
||||
version = "0.1.24"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
||||
@ -97,8 +97,8 @@ fn main() -> Result<()> {
|
||||
|
||||
fn action_toggle_folder(state: &mut AppState) {
|
||||
if let Some(item) = state.get_selected_item() {
|
||||
if item.node.is_dir {
|
||||
let path = item.node.path.clone();
|
||||
if item.is_dir {
|
||||
let path = item.path.clone();
|
||||
if state.expanded_dirs.contains(&path) {
|
||||
// Folder is open, close it
|
||||
state.collapse_selected();
|
||||
@ -936,7 +936,7 @@ fn handle_mouse_event(state: &mut AppState, mouse: MouseEvent, title_bar_area: r
|
||||
if is_double_click {
|
||||
// Double click = toggle folder or play file
|
||||
if let Some(item) = state.get_selected_item() {
|
||||
if item.node.is_dir {
|
||||
if item.is_dir {
|
||||
action_toggle_folder(state);
|
||||
} else {
|
||||
action_play_selection(state, player)?;
|
||||
|
||||
110
src/state/mod.rs
110
src/state/mod.rs
@ -1,7 +1,7 @@
|
||||
use crate::cache::{Cache, FileTreeNode};
|
||||
use crate::config::Config;
|
||||
use std::collections::HashSet;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::Instant;
|
||||
|
||||
// Fuzzy match scoring bonuses
|
||||
@ -96,7 +96,9 @@ pub struct AppState {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct FlattenedItem {
|
||||
pub node: FileTreeNode,
|
||||
pub path: PathBuf,
|
||||
pub name: String,
|
||||
pub is_dir: bool,
|
||||
pub depth: usize,
|
||||
}
|
||||
|
||||
@ -336,11 +338,29 @@ impl AppState {
|
||||
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) {
|
||||
let item = self.get_selected_item().cloned();
|
||||
if let Some(item) = item {
|
||||
if item.node.is_dir {
|
||||
let path = item.node.path.clone();
|
||||
if item.is_dir {
|
||||
let path = item.path.clone();
|
||||
let was_expanded = self.expanded_dirs.contains(&path);
|
||||
|
||||
if was_expanded {
|
||||
@ -348,7 +368,7 @@ impl AppState {
|
||||
self.expanded_dirs.remove(&path);
|
||||
self.rebuild_flattened_items();
|
||||
// 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;
|
||||
}
|
||||
} else {
|
||||
@ -358,19 +378,19 @@ impl AppState {
|
||||
self.expanded_dirs.remove(&parent_buf);
|
||||
self.rebuild_flattened_items();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 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();
|
||||
self.expanded_dirs.remove(&parent_buf);
|
||||
self.rebuild_flattened_items();
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -380,8 +400,8 @@ impl AppState {
|
||||
|
||||
pub fn expand_selected(&mut self) {
|
||||
if let Some(item) = self.get_selected_item() {
|
||||
if item.node.is_dir {
|
||||
let path = item.node.path.clone();
|
||||
if item.is_dir {
|
||||
let path = item.path.clone();
|
||||
self.expanded_dirs.insert(path);
|
||||
self.rebuild_flattened_items();
|
||||
}
|
||||
@ -401,8 +421,8 @@ impl AppState {
|
||||
self.marked_files.clear();
|
||||
// Mark current file
|
||||
if let Some(item) = self.get_selected_item() {
|
||||
if !item.node.is_dir {
|
||||
self.marked_files.insert(item.node.path.clone());
|
||||
if !item.is_dir {
|
||||
self.marked_files.insert(item.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -418,8 +438,8 @@ impl AppState {
|
||||
|
||||
for i in start..=end {
|
||||
if let Some(item) = self.flattened_items.get(i) {
|
||||
if !item.node.is_dir {
|
||||
self.marked_files.insert(item.node.path.clone());
|
||||
if !item.is_dir {
|
||||
self.marked_files.insert(item.path.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -439,15 +459,19 @@ impl AppState {
|
||||
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 (allow duplicates)
|
||||
let mut files = collect_files_from_node(&node);
|
||||
files.sort();
|
||||
self.playlist.extend(files);
|
||||
let path = item.path.clone();
|
||||
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)
|
||||
let mut files = collect_files_from_node(node);
|
||||
files.sort();
|
||||
self.playlist.extend(files);
|
||||
}
|
||||
} else {
|
||||
// Add single file (allow duplicates)
|
||||
self.playlist.push(node.path.clone());
|
||||
self.playlist.push(path);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -468,22 +492,24 @@ impl AppState {
|
||||
self.current_file = None;
|
||||
}
|
||||
} else if let Some(item) = self.get_selected_item() {
|
||||
let node = item.node.clone();
|
||||
if node.is_dir {
|
||||
// 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());
|
||||
} else {
|
||||
// Empty directory
|
||||
self.current_file = None;
|
||||
let path = item.path.clone();
|
||||
let is_dir = item.is_dir;
|
||||
if is_dir {
|
||||
// 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_scroll_offset = 0;
|
||||
self.selected_playlist_index = 0;
|
||||
if let Some(first) = self.playlist.first() {
|
||||
self.current_file = Some(first.clone());
|
||||
} else {
|
||||
// Empty directory
|
||||
self.current_file = None;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Play single file
|
||||
let path = node.path.clone();
|
||||
self.playlist = vec![path.clone()];
|
||||
self.playlist_index = 0;
|
||||
self.playlist_scroll_offset = 0;
|
||||
@ -674,7 +700,7 @@ 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) {
|
||||
if let Some(idx) = self.flattened_items.iter().position(|item| item.path == best_match) {
|
||||
self.selected_index = idx;
|
||||
|
||||
// Scroll to show the match
|
||||
@ -741,7 +767,7 @@ impl AppState {
|
||||
self.rebuild_flattened_items();
|
||||
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
@ -766,7 +792,7 @@ impl AppState {
|
||||
self.rebuild_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;
|
||||
|
||||
// Scroll to show the match
|
||||
@ -813,7 +839,7 @@ impl AppState {
|
||||
self.rebuild_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;
|
||||
|
||||
// Scroll to show the match
|
||||
@ -860,7 +886,7 @@ impl AppState {
|
||||
self.rebuild_flattened_items();
|
||||
|
||||
// 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;
|
||||
|
||||
// Scroll to show the match
|
||||
@ -903,7 +929,7 @@ impl AppState {
|
||||
self.rebuild_flattened_items();
|
||||
|
||||
// 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;
|
||||
|
||||
// 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);
|
||||
|
||||
result.push(FlattenedItem {
|
||||
node: node.clone(),
|
||||
path: node.path.clone(),
|
||||
name: node.name.clone(),
|
||||
is_dir: node.is_dir,
|
||||
depth,
|
||||
});
|
||||
|
||||
|
||||
@ -142,15 +142,15 @@ fn render_file_panel(frame: &mut Frame, state: &mut AppState, area: Rect) {
|
||||
.map(|(display_idx, item)| {
|
||||
let idx = state.scroll_offset + display_idx;
|
||||
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
|
||||
// Only show selection bar when file panel has focus
|
||||
let is_selected = !state.focus_playlist && idx == state.selected_index;
|
||||
|
||||
// Add icon for directories and files
|
||||
let icon = if item.node.is_dir {
|
||||
let is_expanded = state.expanded_dirs.contains(&item.node.path);
|
||||
let icon = if item.is_dir {
|
||||
let is_expanded = state.expanded_dirs.contains(&item.path);
|
||||
// Nerd font folder icons: \u{eaf7} = open, \u{ea83} = closed
|
||||
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 {
|
||||
// File icons based on extension
|
||||
let extension = item.node.path.extension()
|
||||
let extension = item.path.extension()
|
||||
.and_then(|e| e.to_str())
|
||||
.unwrap_or("")
|
||||
.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() {
|
||||
highlight_search_matches(&item.node.name, &search_query, is_selected)
|
||||
highlight_search_matches(&item.name, &search_query, is_selected)
|
||||
} 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 {
|
||||
// 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 {
|
||||
Theme::selected()
|
||||
}
|
||||
} else if state.marked_files.contains(&item.node.path) {
|
||||
} else if state.marked_files.contains(&item.path) {
|
||||
Theme::marked()
|
||||
} else {
|
||||
Theme::secondary()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user