mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-07-26 05:46:59 +00:00
feat(gui): better search with automatic tree expansion, directories scanning and caching
This commit is contained in:
Generated
+1
@@ -3242,6 +3242,7 @@ name = "pwsp-gui"
|
|||||||
version = "1.13.0"
|
version = "1.13.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"dirs",
|
||||||
"eframe",
|
"eframe",
|
||||||
"egui",
|
"egui",
|
||||||
"egui_dnd",
|
"egui_dnd",
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ anyhow.workspace = true
|
|||||||
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
dirs.workspace = true
|
||||||
|
|
||||||
egui.workspace = true
|
egui.workspace = true
|
||||||
eframe.workspace = true
|
eframe.workspace = true
|
||||||
|
|||||||
@@ -353,6 +353,17 @@ kz = "Ыстық пернелерді іздеу..."
|
|||||||
he = "חפש מקשי קיצור..."
|
he = "חפש מקשי קיצור..."
|
||||||
pt-BR = "Buscar atalhos..."
|
pt-BR = "Buscar atalhos..."
|
||||||
|
|
||||||
|
[gui.search_truncated_warning]
|
||||||
|
en = "... and %{count} more results"
|
||||||
|
ru = "... и ещё %{count} результатов"
|
||||||
|
es = "... y %{count} resultados más"
|
||||||
|
fr = "... et %{count} résultats de plus"
|
||||||
|
zh = "... 还有 %{count} 个结果"
|
||||||
|
ar = "... و %{count} نتائج أخرى"
|
||||||
|
kz = "... және тағы %{count} нәтиже"
|
||||||
|
he = "... ועוד %{count} תוצאות"
|
||||||
|
pt-BR = "... e mais %{count} resultados"
|
||||||
|
|
||||||
[gui.hotkeys.add_command_select]
|
[gui.hotkeys.add_command_select]
|
||||||
en = "Add Command"
|
en = "Add Command"
|
||||||
ru = "Добавить команду"
|
ru = "Добавить команду"
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_input(&mut self, ctx: &Context) {
|
pub fn handle_input(&mut self, ctx: &Context) {
|
||||||
let _modifiers = self.modifiers(ctx);
|
let modifiers = self.modifiers(ctx);
|
||||||
let search_focused = {
|
let search_focused = {
|
||||||
if let Some(focused_id) = self.get_focused(ctx)
|
if let Some(focused_id) = self.get_focused(ctx)
|
||||||
&& let Some(search_id) = self.app_state.search_field_id
|
&& let Some(search_id) = self.app_state.search_field_id
|
||||||
@@ -185,14 +185,15 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Focus search field
|
// Focus search field
|
||||||
if self.key_pressed(ctx, Key::Slash) {
|
if modifiers.ctrl && self.key_pressed(ctx, Key::F) {
|
||||||
if search_focused {
|
self.app_state.force_focus_search = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unfocus search field
|
||||||
|
if search_focused && self.key_pressed(ctx, Key::Escape) {
|
||||||
ctx.memory_mut(|m| {
|
ctx.memory_mut(|m| {
|
||||||
m.request_focus(Id::NULL);
|
m.request_focus(Id::NULL);
|
||||||
});
|
});
|
||||||
} else {
|
|
||||||
self.app_state.force_focus_search = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for hotkey chord triggers
|
// Check for hotkey chord triggers
|
||||||
|
|||||||
+154
-18
@@ -21,9 +21,12 @@ use pwsp_lib::{
|
|||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
fs,
|
fs,
|
||||||
|
hash::{DefaultHasher, Hash, Hasher},
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
|
thread,
|
||||||
};
|
};
|
||||||
use system_fonts::{FontStyle, FoundFontSource, find_for_locale};
|
use system_fonts::{FontStyle, FoundFontSource, find_for_locale};
|
||||||
|
|
||||||
@@ -31,6 +34,16 @@ const SUPPORTED_EXTENSIONS: [&str; 13] = [
|
|||||||
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
|
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi", "opus",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
fn get_cache_file_for(path: &Path) -> PathBuf {
|
||||||
|
let mut hasher = DefaultHasher::new();
|
||||||
|
path.hash(&mut hasher);
|
||||||
|
let hash = hasher.finish();
|
||||||
|
dirs::cache_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||||
|
.join("pwsp")
|
||||||
|
.join(format!("{:x}.json", hash))
|
||||||
|
}
|
||||||
|
|
||||||
struct SoundpadGui {
|
struct SoundpadGui {
|
||||||
pub app_state: AppState,
|
pub app_state: AppState,
|
||||||
pub config: GuiConfig,
|
pub config: GuiConfig,
|
||||||
@@ -106,20 +119,120 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn open_dir(&mut self, path: &PathBuf) {
|
pub fn open_dir(&mut self, path: &PathBuf, ctx: Option<Context>, force_rescan: bool) {
|
||||||
self.app_state.current_dir = Some(path.clone());
|
self.app_state.current_dir = Some(path.clone());
|
||||||
match path.read_dir() {
|
|
||||||
|
if !self.app_state.dir_cache.contains_key(path) || force_rescan {
|
||||||
|
match fs::read_dir(path) {
|
||||||
Ok(read_dir) => {
|
Ok(read_dir) => {
|
||||||
self.app_state.listed_files = read_dir
|
let files = read_dir
|
||||||
.filter_map(|res| res.ok())
|
.filter_map(|res| res.ok())
|
||||||
.map(|entry| entry.path())
|
.map(|entry| entry.path())
|
||||||
.collect();
|
.collect::<Vec<_>>();
|
||||||
|
self.app_state.listed_files = files.iter().cloned().collect();
|
||||||
|
self.app_state.dir_cache.insert(path.clone(), files);
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to read directory {:?}: {}", path, e);
|
eprintln!("Failed to read directory {:?}: {}", path, e);
|
||||||
self.app_state.listed_files.clear();
|
self.app_state.listed_files.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
self.app_state.listed_files = self
|
||||||
|
.app_state
|
||||||
|
.dir_cache
|
||||||
|
.get(path)
|
||||||
|
.unwrap()
|
||||||
|
.clone()
|
||||||
|
.into_iter()
|
||||||
|
.collect();
|
||||||
|
}
|
||||||
|
|
||||||
|
let is_scanning = self.app_state.scanning_dirs.lock().unwrap().contains(path);
|
||||||
|
let is_cached = self.app_state.recursive_files_cache.contains_key(path);
|
||||||
|
let has_scanned_this_session = self.app_state.scanned_this_session.contains(path);
|
||||||
|
|
||||||
|
let needs_scan = force_rescan || !has_scanned_this_session || !is_cached;
|
||||||
|
|
||||||
|
if !is_scanning && needs_scan {
|
||||||
|
self.app_state
|
||||||
|
.scanning_dirs
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.insert(path.clone());
|
||||||
|
self.app_state.scanned_this_session.insert(path.clone());
|
||||||
|
|
||||||
|
let finished_scans = self.app_state.finished_scans.clone();
|
||||||
|
let scanning_dirs = self.app_state.scanning_dirs.clone();
|
||||||
|
let path_clone = path.clone();
|
||||||
|
|
||||||
|
thread::spawn(move || {
|
||||||
|
let cache_file = get_cache_file_for(&path_clone);
|
||||||
|
|
||||||
|
// 1. Try to load from disk cache if we don't have it in memory yet
|
||||||
|
if !is_cached
|
||||||
|
&& let Ok(data) = fs::read_to_string(&cache_file)
|
||||||
|
&& let Ok((all_files, dir_updates)) = serde_json::from_str::<(
|
||||||
|
Vec<PathBuf>,
|
||||||
|
HashMap<PathBuf, Vec<PathBuf>>,
|
||||||
|
)>(&data)
|
||||||
|
{
|
||||||
|
finished_scans.lock().unwrap().push((
|
||||||
|
path_clone.clone(),
|
||||||
|
all_files,
|
||||||
|
dir_updates,
|
||||||
|
));
|
||||||
|
if let Some(ctx) = ctx.as_ref() {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Scan recursively
|
||||||
|
let mut all_files = Vec::new();
|
||||||
|
let mut dir_updates = HashMap::new();
|
||||||
|
let mut dirs_to_visit = vec![path_clone.clone()];
|
||||||
|
|
||||||
|
while let Some(dir) = dirs_to_visit.pop() {
|
||||||
|
if let Ok(entries) = fs::read_dir(&dir) {
|
||||||
|
let mut children = Vec::new();
|
||||||
|
for entry in entries.filter_map(|e| e.ok()) {
|
||||||
|
let p = entry.path();
|
||||||
|
if p.is_dir() {
|
||||||
|
dirs_to_visit.push(p.clone());
|
||||||
|
children.push(p);
|
||||||
|
} else if crate::gui::SUPPORTED_EXTENSIONS.contains(
|
||||||
|
&p.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
all_files.push(p.clone());
|
||||||
|
children.push(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dir_updates.insert(dir, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Save to disk cache
|
||||||
|
if let Some(parent) = cache_file.parent() {
|
||||||
|
fs::create_dir_all(parent).ok();
|
||||||
|
}
|
||||||
|
if let Ok(json) = serde_json::to_string(&(all_files.clone(), dir_updates.clone())) {
|
||||||
|
fs::write(&cache_file, json).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Send to UI
|
||||||
|
finished_scans
|
||||||
|
.lock()
|
||||||
|
.unwrap()
|
||||||
|
.push((path_clone.clone(), all_files, dir_updates));
|
||||||
|
scanning_dirs.lock().unwrap().remove(&path_clone);
|
||||||
|
if let Some(ctx) = ctx {
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn play_file(&mut self, path: &Path, concurrent: bool) {
|
pub fn play_file(&mut self, path: &Path, concurrent: bool) {
|
||||||
@@ -157,7 +270,14 @@ impl SoundpadGui {
|
|||||||
make_request_async(Request::play_hotkey(slot));
|
make_request_async(Request::play_hotkey(slot));
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
pub fn get_filtered_files(
|
||||||
|
&self,
|
||||||
|
matching_dirs: &HashSet<PathBuf>,
|
||||||
|
matching_files: &HashSet<PathBuf>,
|
||||||
|
) -> Vec<PathBuf> {
|
||||||
|
let search_query = self.app_state.search_query.to_lowercase();
|
||||||
|
let search_query = search_query.trim();
|
||||||
|
|
||||||
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
||||||
let sort_order = self
|
let sort_order = self
|
||||||
.app_state
|
.app_state
|
||||||
@@ -178,29 +298,42 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let search_query = self.app_state.search_query.to_lowercase();
|
let is_cached = self
|
||||||
let search_query = search_query.trim();
|
.app_state
|
||||||
|
.current_dir
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|d| self.app_state.recursive_files_cache.contains_key(d));
|
||||||
|
|
||||||
|
if !search_query.is_empty() && is_cached {
|
||||||
|
files
|
||||||
|
.into_iter()
|
||||||
|
.filter(|p| {
|
||||||
|
if p.is_dir() {
|
||||||
|
matching_dirs.contains(p)
|
||||||
|
} else {
|
||||||
|
matching_files.contains(p)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
files
|
files
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|entry_path| {
|
.filter(|entry_path| {
|
||||||
if entry_path.is_dir() {
|
if !entry_path.is_dir()
|
||||||
return true;
|
&& !SUPPORTED_EXTENSIONS.contains(
|
||||||
}
|
|
||||||
|
|
||||||
if !SUPPORTED_EXTENSIONS.contains(
|
|
||||||
&entry_path
|
&entry_path
|
||||||
.extension()
|
.extension()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_str()
|
.to_str()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
) {
|
)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !search_query.is_empty() {
|
if !search_query.is_empty() {
|
||||||
let file_name = entry_path.file_name().unwrap_or_default().to_string_lossy();
|
let file_name =
|
||||||
|
entry_path.file_name().unwrap_or_default().to_string_lossy();
|
||||||
if !file_name.to_lowercase().contains(search_query) {
|
if !file_name.to_lowercase().contains(search_query) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -210,6 +343,7 @@ impl SoundpadGui {
|
|||||||
})
|
})
|
||||||
.collect()
|
.collect()
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
|
fn add_font(font_name: &str, font_bytes: &[u8], fonts: &mut FontDefinitions) -> Result<()> {
|
||||||
@@ -325,14 +459,16 @@ mod tests {
|
|||||||
// On the real OS filesystem, these paths don't exist, so they are treated as files.
|
// On the real OS filesystem, these paths don't exist, so they are treated as files.
|
||||||
// Unsupported extensions (like .txt) will be filtered out.
|
// Unsupported extensions (like .txt) will be filtered out.
|
||||||
// So we expect only file_b and file_c, sorted alphabetically.
|
// So we expect only file_b and file_c, sorted alphabetically.
|
||||||
let filtered = gui.get_filtered_files();
|
let empty_dirs = HashSet::new();
|
||||||
|
let empty_files = HashSet::new();
|
||||||
|
let filtered = gui.get_filtered_files(&empty_dirs, &empty_files);
|
||||||
assert_eq!(filtered.len(), 2);
|
assert_eq!(filtered.len(), 2);
|
||||||
assert_eq!(filtered[0], file_b);
|
assert_eq!(filtered[0], file_b);
|
||||||
assert_eq!(filtered[1], file_c);
|
assert_eq!(filtered[1], file_c);
|
||||||
|
|
||||||
// Test search query
|
// Test search query
|
||||||
gui.app_state.search_query = "c_fi".to_string();
|
gui.app_state.search_query = "c_fi".to_string();
|
||||||
let filtered_search = gui.get_filtered_files();
|
let filtered_search = gui.get_filtered_files(&empty_dirs, &empty_files);
|
||||||
assert_eq!(filtered_search.len(), 1);
|
assert_eq!(filtered_search.len(), 1);
|
||||||
assert_eq!(filtered_search[0], file_c);
|
assert_eq!(filtered_search[0], file_c);
|
||||||
|
|
||||||
@@ -345,7 +481,7 @@ mod tests {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
gui.app_state.search_query = String::new();
|
gui.app_state.search_query = String::new();
|
||||||
let filtered_desc = gui.get_filtered_files();
|
let filtered_desc = gui.get_filtered_files(&empty_dirs, &empty_files);
|
||||||
assert_eq!(filtered_desc.len(), 2);
|
assert_eq!(filtered_desc.len(), 2);
|
||||||
assert_eq!(filtered_desc[0], file_c);
|
assert_eq!(filtered_desc[0], file_c);
|
||||||
assert_eq!(filtered_desc[1], file_b);
|
assert_eq!(filtered_desc[1], file_b);
|
||||||
|
|||||||
+183
-17
@@ -10,7 +10,7 @@ use pwsp_lib::types::{
|
|||||||
gui::{AppState, AudioPlayerState},
|
gui::{AppState, AudioPlayerState},
|
||||||
};
|
};
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
use std::{cmp::Ordering, path::Path, path::PathBuf};
|
use std::{cmp::Ordering, collections::HashSet, path::Path, path::PathBuf};
|
||||||
|
|
||||||
pub(crate) enum FileAction {
|
pub(crate) enum FileAction {
|
||||||
Play(PathBuf, bool),
|
Play(PathBuf, bool),
|
||||||
@@ -195,7 +195,7 @@ impl SoundpadGui {
|
|||||||
.sort_order = order;
|
.sort_order = order;
|
||||||
self.config.save_to_file().ok();
|
self.config.save_to_file().ok();
|
||||||
self.app_state.dir_cache.remove(path);
|
self.app_state.dir_cache.remove(path);
|
||||||
self.open_dir(path);
|
self.open_dir(path, Some(ui.ctx().clone()), false);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -203,7 +203,7 @@ impl SoundpadGui {
|
|||||||
self.app_state.dirs = dirs;
|
self.app_state.dirs = dirs;
|
||||||
|
|
||||||
if let Some(path) = dir_to_open {
|
if let Some(path) = dir_to_open {
|
||||||
self.open_dir(&path);
|
self.open_dir(&path, Some(ui.ctx().clone()), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
@@ -227,12 +227,41 @@ impl SoundpadGui {
|
|||||||
|
|
||||||
fn draw_files_search_field(&mut self, ui: &mut Ui) {
|
fn draw_files_search_field(&mut self, ui: &mut Ui) {
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
let is_scanning = self
|
||||||
|
.app_state
|
||||||
|
.current_dir
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|d| self.app_state.scanning_dirs.lock().unwrap().contains(d));
|
||||||
|
let spinner_width = if is_scanning { 20.0 } else { 0.0 };
|
||||||
|
|
||||||
|
let refresh_button = Button::new(ICON_REFRESH).frame(false);
|
||||||
|
let refresh_width = 24.0;
|
||||||
|
|
||||||
let search_field_response = ui.add_sized(
|
let search_field_response = ui.add_sized(
|
||||||
[ui.available_width(), 22.0],
|
[
|
||||||
|
f32::max(
|
||||||
|
0.0,
|
||||||
|
ui.available_width() - spinner_width - refresh_width - 8.0,
|
||||||
|
),
|
||||||
|
22.0,
|
||||||
|
],
|
||||||
TextEdit::singleline(&mut self.app_state.search_query)
|
TextEdit::singleline(&mut self.app_state.search_query)
|
||||||
.hint_text(t!("gui.search_placeholder")),
|
.hint_text(t!("gui.search_placeholder")),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if is_scanning {
|
||||||
|
ui.spinner();
|
||||||
|
}
|
||||||
|
|
||||||
|
let refresh_response = ui
|
||||||
|
.add_sized([refresh_width, 22.0], refresh_button)
|
||||||
|
.on_hover_text("Refresh");
|
||||||
|
if refresh_response.clicked()
|
||||||
|
&& let Some(dir) = self.app_state.current_dir.clone()
|
||||||
|
{
|
||||||
|
self.open_dir(&dir, Some(ui.ctx().clone()), true);
|
||||||
|
}
|
||||||
|
|
||||||
if self.app_state.force_focus_search {
|
if self.app_state.force_focus_search {
|
||||||
search_field_response.request_focus();
|
search_field_response.request_focus();
|
||||||
self.app_state.force_focus_search = false;
|
self.app_state.force_focus_search = false;
|
||||||
@@ -247,9 +276,54 @@ impl SoundpadGui {
|
|||||||
ui.set_min_width(area_size.x);
|
ui.set_min_width(area_size.x);
|
||||||
ui.set_min_height(area_size.y);
|
ui.set_min_height(area_size.y);
|
||||||
|
|
||||||
|
let finished = {
|
||||||
|
let mut scans = self.app_state.finished_scans.lock().unwrap();
|
||||||
|
std::mem::take(&mut *scans)
|
||||||
|
};
|
||||||
|
for (top_dir, all_files, dir_updates) in finished {
|
||||||
|
self.app_state
|
||||||
|
.recursive_files_cache
|
||||||
|
.insert(top_dir, all_files);
|
||||||
|
self.app_state.dir_cache.extend(dir_updates);
|
||||||
|
}
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
let mut actions = Vec::new();
|
let mut actions = Vec::new();
|
||||||
let files = self.get_filtered_files();
|
|
||||||
|
let search_query = self.app_state.search_query.to_lowercase();
|
||||||
|
let search_query = search_query.trim();
|
||||||
|
|
||||||
|
let mut matching_dirs = std::collections::HashSet::new();
|
||||||
|
let mut matching_files = std::collections::HashSet::new();
|
||||||
|
|
||||||
|
if !search_query.is_empty()
|
||||||
|
&& let Some(current_dir) = &self.app_state.current_dir
|
||||||
|
&& let Some(all_files) = self.app_state.recursive_files_cache.get(current_dir)
|
||||||
|
{
|
||||||
|
for file in all_files {
|
||||||
|
if let Ok(rel_path) = file.strip_prefix(current_dir) {
|
||||||
|
let rel_path_str =
|
||||||
|
rel_path.to_string_lossy().to_lowercase().replace("\\", "/");
|
||||||
|
if rel_path_str.contains(search_query) {
|
||||||
|
matching_files.insert(file.clone());
|
||||||
|
let mut p = file.parent();
|
||||||
|
while let Some(parent) = p {
|
||||||
|
if parent == current_dir {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
matching_dirs.insert(parent.to_path_buf());
|
||||||
|
p = parent.parent();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let max_results = 300;
|
||||||
|
let is_truncated = !search_query.is_empty() && matching_files.len() > max_results;
|
||||||
|
|
||||||
|
let files = self.get_filtered_files(&matching_dirs, &matching_files);
|
||||||
|
let mut rendered_count = 0;
|
||||||
for entry_path in files {
|
for entry_path in files {
|
||||||
Self::draw_tree_node(
|
Self::draw_tree_node(
|
||||||
ui,
|
ui,
|
||||||
@@ -258,6 +332,22 @@ impl SoundpadGui {
|
|||||||
&mut self.app_state,
|
&mut self.app_state,
|
||||||
&self.audio_player_state,
|
&self.audio_player_state,
|
||||||
&mut actions,
|
&mut actions,
|
||||||
|
&matching_dirs,
|
||||||
|
&matching_files,
|
||||||
|
search_query,
|
||||||
|
&mut rendered_count,
|
||||||
|
max_results,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if is_truncated {
|
||||||
|
ui.add_space(8.0);
|
||||||
|
let text = t!(
|
||||||
|
"gui.search_truncated_warning",
|
||||||
|
count = matching_files.len() - max_results
|
||||||
|
);
|
||||||
|
ui.label(
|
||||||
|
egui::RichText::new(text).color(egui::Color32::from_rgb(200, 150, 50)),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,6 +376,7 @@ impl SoundpadGui {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn draw_tree_node_dir(
|
fn draw_tree_node_dir(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
@@ -293,15 +384,39 @@ impl SoundpadGui {
|
|||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
audio_player_state: &AudioPlayerState,
|
audio_player_state: &AudioPlayerState,
|
||||||
actions: &mut Vec<FileAction>,
|
actions: &mut Vec<FileAction>,
|
||||||
|
matching_dirs: &std::collections::HashSet<PathBuf>,
|
||||||
|
matching_files: &std::collections::HashSet<PathBuf>,
|
||||||
|
search_query: &str,
|
||||||
|
rendered_count: &mut usize,
|
||||||
|
max_results: usize,
|
||||||
) {
|
) {
|
||||||
|
if *rendered_count >= max_results && !search_query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
let dir_name = path
|
let dir_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_string();
|
||||||
CollapsingHeader::new(dir_name)
|
|
||||||
.id_salt(&path)
|
let id_salt = if search_query.is_empty() {
|
||||||
.show(ui, |ui| {
|
format!("normal_{:?}", path)
|
||||||
|
} else {
|
||||||
|
format!("search_{:?}", path)
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut header = CollapsingHeader::new(dir_name).id_salt(id_salt);
|
||||||
|
|
||||||
|
let is_cached = app_state
|
||||||
|
.current_dir
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|d| app_state.recursive_files_cache.contains_key(d));
|
||||||
|
if !search_query.is_empty() && is_cached {
|
||||||
|
header = header.default_open(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
header.show(ui, |ui| {
|
||||||
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
|
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
|
||||||
cached.clone()
|
cached.clone()
|
||||||
} else {
|
} else {
|
||||||
@@ -339,21 +454,40 @@ impl SoundpadGui {
|
|||||||
read
|
read
|
||||||
};
|
};
|
||||||
|
|
||||||
let search_query = app_state.search_query.to_lowercase();
|
|
||||||
let search_query = search_query.trim();
|
|
||||||
|
|
||||||
for child in children {
|
for child in children {
|
||||||
if !child.is_dir() && !search_query.is_empty() {
|
if *rendered_count >= max_results && !search_query.is_empty() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if !search_query.is_empty() && is_cached {
|
||||||
|
if child.is_dir() && !matching_dirs.contains(&child) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !child.is_dir() && !matching_files.contains(&child) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else if !search_query.is_empty() && !child.is_dir() {
|
||||||
let file_name = child
|
let file_name = child
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
.to_string_lossy()
|
.to_string_lossy()
|
||||||
.to_string();
|
.to_lowercase();
|
||||||
if !file_name.to_lowercase().contains(search_query) {
|
if !file_name.contains(search_query) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions);
|
Self::draw_tree_node(
|
||||||
|
ui,
|
||||||
|
child,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
audio_player_state,
|
||||||
|
actions,
|
||||||
|
matching_dirs,
|
||||||
|
matching_files,
|
||||||
|
search_query,
|
||||||
|
rendered_count,
|
||||||
|
max_results,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -364,7 +498,10 @@ impl SoundpadGui {
|
|||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
audio_player_state: &AudioPlayerState,
|
audio_player_state: &AudioPlayerState,
|
||||||
actions: &mut Vec<FileAction>,
|
actions: &mut Vec<FileAction>,
|
||||||
|
rendered_count: &mut usize,
|
||||||
) {
|
) {
|
||||||
|
*rendered_count += 1;
|
||||||
|
|
||||||
let file_name = path
|
let file_name = path
|
||||||
.file_name()
|
.file_name()
|
||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
@@ -500,6 +637,7 @@ impl SoundpadGui {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn draw_tree_node(
|
fn draw_tree_node(
|
||||||
ui: &mut Ui,
|
ui: &mut Ui,
|
||||||
path: std::path::PathBuf,
|
path: std::path::PathBuf,
|
||||||
@@ -507,11 +645,39 @@ impl SoundpadGui {
|
|||||||
app_state: &mut AppState,
|
app_state: &mut AppState,
|
||||||
audio_player_state: &AudioPlayerState,
|
audio_player_state: &AudioPlayerState,
|
||||||
actions: &mut Vec<FileAction>,
|
actions: &mut Vec<FileAction>,
|
||||||
|
matching_dirs: &HashSet<PathBuf>,
|
||||||
|
matching_files: &HashSet<PathBuf>,
|
||||||
|
search_query: &str,
|
||||||
|
rendered_count: &mut usize,
|
||||||
|
max_results: usize,
|
||||||
) {
|
) {
|
||||||
|
if *rendered_count >= max_results && !search_query.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if path.is_dir() {
|
if path.is_dir() {
|
||||||
Self::draw_tree_node_dir(ui, path, config, app_state, audio_player_state, actions);
|
Self::draw_tree_node_dir(
|
||||||
|
ui,
|
||||||
|
path,
|
||||||
|
config,
|
||||||
|
app_state,
|
||||||
|
audio_player_state,
|
||||||
|
actions,
|
||||||
|
matching_dirs,
|
||||||
|
matching_files,
|
||||||
|
search_query,
|
||||||
|
rendered_count,
|
||||||
|
max_results,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
Self::draw_tree_node_file(
|
||||||
|
ui,
|
||||||
|
path,
|
||||||
|
app_state,
|
||||||
|
audio_player_state,
|
||||||
|
actions,
|
||||||
|
rendered_count,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,12 @@ use egui::Id;
|
|||||||
use std::{
|
use std::{
|
||||||
collections::{HashMap, HashSet},
|
collections::{HashMap, HashSet},
|
||||||
path::PathBuf,
|
path::PathBuf,
|
||||||
|
sync::{Arc, Mutex},
|
||||||
time::Instant,
|
time::Instant,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub type ScanResult = (PathBuf, Vec<PathBuf>, HashMap<PathBuf, Vec<PathBuf>>);
|
||||||
|
|
||||||
#[derive(Default, Debug)]
|
#[derive(Default, Debug)]
|
||||||
pub struct TrackUiState {
|
pub struct TrackUiState {
|
||||||
pub position_slider_value: f32,
|
pub position_slider_value: f32,
|
||||||
@@ -50,6 +53,11 @@ pub struct AppState {
|
|||||||
pub listed_dirs: HashSet<PathBuf>,
|
pub listed_dirs: HashSet<PathBuf>,
|
||||||
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
|
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
|
||||||
|
|
||||||
|
pub scanning_dirs: Arc<Mutex<HashSet<PathBuf>>>,
|
||||||
|
pub scanned_this_session: HashSet<PathBuf>,
|
||||||
|
pub finished_scans: Arc<Mutex<Vec<ScanResult>>>,
|
||||||
|
pub recursive_files_cache: HashMap<PathBuf, Vec<PathBuf>>,
|
||||||
|
|
||||||
pub show_hotkeys: bool,
|
pub show_hotkeys: bool,
|
||||||
pub hotkey_capture_active: bool,
|
pub hotkey_capture_active: bool,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user