mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-06-19 12:13:32 +00:00
feat: recursively show directories in files list (#105)
This commit is contained in:
committed by
GitHub
parent
d974a93c04
commit
8155cceac8
+230
-125
@@ -6,11 +6,16 @@ use egui::{
|
|||||||
use egui_dnd::dnd;
|
use egui_dnd::dnd;
|
||||||
use egui_extras::{Column, TableBuilder};
|
use egui_extras::{Column, TableBuilder};
|
||||||
use egui_material_icons::icons::*;
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::gui::AudioPlayerState;
|
||||||
use pwsp::types::socket::Request;
|
use pwsp::types::socket::Request;
|
||||||
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
|
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
|
||||||
use pwsp::utils::gui::{format_time_pair, make_request_async};
|
use pwsp::utils::gui::{format_time_pair, make_request_async};
|
||||||
use rust_i18n::t;
|
use rust_i18n::t;
|
||||||
use std::{path::Path, time::Instant};
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
|
path::{Path, PathBuf},
|
||||||
|
time::Instant,
|
||||||
|
};
|
||||||
|
|
||||||
enum TrackAction {
|
enum TrackAction {
|
||||||
Pause(u32),
|
Pause(u32),
|
||||||
@@ -26,6 +31,13 @@ enum HotkeyAction {
|
|||||||
Play(String),
|
Play(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum FileAction {
|
||||||
|
Play(PathBuf, bool),
|
||||||
|
StopAndPlay(u32, PathBuf, bool),
|
||||||
|
AssignHotkey(PathBuf),
|
||||||
|
SetSelected(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
impl SoundpadGui {
|
impl SoundpadGui {
|
||||||
fn get_volume_icon(volume: f32) -> &'static str {
|
fn get_volume_icon(volume: f32) -> &'static str {
|
||||||
if volume > 0.7 {
|
if volume > 0.7 {
|
||||||
@@ -782,144 +794,237 @@ impl SoundpadGui {
|
|||||||
ui.set_min_height(area_size.y);
|
ui.set_min_height(area_size.y);
|
||||||
|
|
||||||
ui.vertical(|ui| {
|
ui.vertical(|ui| {
|
||||||
|
let mut actions = Vec::new();
|
||||||
let files = self.get_filtered_files();
|
let files = self.get_filtered_files();
|
||||||
|
|
||||||
for entry_path in files {
|
for entry_path in files {
|
||||||
let file_name = entry_path
|
Self::draw_tree_node(
|
||||||
.file_name()
|
ui,
|
||||||
.unwrap_or_default()
|
entry_path,
|
||||||
.to_string_lossy()
|
&mut self.app_state,
|
||||||
.to_string();
|
&self.audio_player_state,
|
||||||
|
&mut actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
ui.horizontal(|ui| {
|
for action in actions {
|
||||||
// Hotkey badge
|
match action {
|
||||||
let hotkey_badge = self.get_hotkey_badge(&entry_path);
|
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent),
|
||||||
if let Some(badge) = &hotkey_badge {
|
FileAction::StopAndPlay(id, path, concurrent) => {
|
||||||
ui.label(
|
self.stop(Some(id));
|
||||||
RichText::new(badge)
|
self.play_file(&path, concurrent);
|
||||||
.small()
|
|
||||||
.monospace()
|
|
||||||
.color(Color32::from_rgb(100, 200, 100)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
FileAction::AssignHotkey(path) => {
|
||||||
let mut file_button_text = RichText::new(&file_name);
|
self.app_state.assigning_hotkey_for_file = Some(path);
|
||||||
if let Some(current_file) = &self.app_state.selected_file
|
self.app_state.hotkey_capture_active = true;
|
||||||
&& current_file.eq(&entry_path)
|
|
||||||
{
|
|
||||||
file_button_text = file_button_text.color(Color32::WHITE);
|
|
||||||
}
|
}
|
||||||
|
FileAction::SetSelected(path) => {
|
||||||
let file_button = Button::new(file_button_text).frame(false).truncate();
|
self.app_state.selected_file = Some(path);
|
||||||
let file_button_response = ui.add(file_button);
|
|
||||||
if file_button_response.clicked() {
|
|
||||||
ui.input(|i| {
|
|
||||||
if i.modifiers.ctrl {
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
} else if i.modifiers.shift
|
|
||||||
&& let Some(last_track) =
|
|
||||||
self.audio_player_state.tracks.last()
|
|
||||||
{
|
|
||||||
self.stop(Some(last_track.id));
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
} else {
|
|
||||||
self.play_file(&entry_path, false);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Context menu
|
|
||||||
file_button_response.context_menu(|ui| {
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_BOLT.codepoint,
|
|
||||||
t!("gui.context.files.play_solo")
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.play_file(&entry_path, false);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_ADD.codepoint,
|
|
||||||
t!("gui.context.files.add_new")
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_SWAP_HORIZ.codepoint,
|
|
||||||
t!("gui.context.files.replace_last")
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
&& let Some(last_track) = self.audio_player_state.tracks.last()
|
|
||||||
{
|
|
||||||
self.stop(Some(last_track.id));
|
|
||||||
self.play_file(&entry_path, true);
|
|
||||||
self.app_state.selected_file = Some(entry_path.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_OPEN_IN_BROWSER.codepoint,
|
|
||||||
t!("gui.context.files.show_in_fm")
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
&& let Err(e) = opener::reveal(&entry_path)
|
|
||||||
{
|
|
||||||
eprintln!("Failed to open file manager: {}", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
ui.separator();
|
|
||||||
|
|
||||||
if ui
|
|
||||||
.button(format!(
|
|
||||||
"{} {}",
|
|
||||||
ICON_KEYBOARD.codepoint,
|
|
||||||
t!("gui.context.files.asign_hotkey")
|
|
||||||
))
|
|
||||||
.clicked()
|
|
||||||
{
|
|
||||||
self.app_state.assigning_hotkey_for_file =
|
|
||||||
Some(entry_path.clone());
|
|
||||||
self.app_state.hotkey_capture_active = true;
|
|
||||||
ui.close();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_hotkey_badge(&self, path: &Path) -> Option<String> {
|
fn draw_tree_node(
|
||||||
for slot in &self.app_state.hotkey_config.slots {
|
ui: &mut Ui,
|
||||||
if slot.action.name == "play"
|
path: std::path::PathBuf,
|
||||||
&& let Some(file_path_str) = slot.action.args.get("file_path")
|
app_state: &mut AppState,
|
||||||
&& Path::new(file_path_str) == path
|
audio_player_state: &AudioPlayerState,
|
||||||
{
|
actions: &mut Vec<FileAction>,
|
||||||
if let Some(chord) = &slot.key_chord {
|
) {
|
||||||
return Some(format!("[{}]", chord));
|
if path.is_dir() {
|
||||||
} else {
|
let dir_name = path
|
||||||
return Some(format!("[{}]", slot.slot));
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
CollapsingHeader::new(dir_name)
|
||||||
|
.id_salt(&path)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
|
||||||
|
cached.clone()
|
||||||
|
} else {
|
||||||
|
let mut read = Vec::new();
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&path) {
|
||||||
|
for entry in entries.filter_map(|e| e.ok()) {
|
||||||
|
read.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.sort_by(|a, b| {
|
||||||
|
let a_is_dir = a.is_dir();
|
||||||
|
let b_is_dir = b.is_dir();
|
||||||
|
if a_is_dir && !b_is_dir {
|
||||||
|
Ordering::Less
|
||||||
|
} else if !a_is_dir && b_is_dir {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
a.cmp(b)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app_state.dir_cache.insert(path.clone(), read.clone());
|
||||||
|
read
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_query = app_state.search_query.to_lowercase();
|
||||||
|
let search_query = search_query.trim();
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
if !child.is_dir() {
|
||||||
|
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
|
||||||
|
&child
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !search_query.is_empty() {
|
||||||
|
let file_name = child
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
if !file_name.to_lowercase().contains(search_query) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Hotkey badge
|
||||||
|
let mut hotkey_badge = None;
|
||||||
|
for slot in &app_state.hotkey_config.slots {
|
||||||
|
if slot.action.name == "play"
|
||||||
|
&& let Some(file_path_str) = slot.action.args.get("file_path")
|
||||||
|
&& Path::new(file_path_str) == path
|
||||||
|
{
|
||||||
|
if let Some(chord) = &slot.key_chord {
|
||||||
|
hotkey_badge = Some(format!("[{}]", chord));
|
||||||
|
} else {
|
||||||
|
hotkey_badge = Some(format!("[{}]", slot.slot));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
if let Some(badge) = &hotkey_badge {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(badge)
|
||||||
|
.small()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::from_rgb(100, 200, 100)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut file_button_text = RichText::new(&file_name);
|
||||||
|
if let Some(current_file) = &app_state.selected_file
|
||||||
|
&& current_file.eq(&path)
|
||||||
|
{
|
||||||
|
file_button_text = file_button_text.color(Color32::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_button = Button::new(file_button_text).frame(false).truncate();
|
||||||
|
let file_button_response = ui.add(file_button);
|
||||||
|
if file_button_response.clicked() {
|
||||||
|
ui.input(|i| {
|
||||||
|
if i.modifiers.ctrl {
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
} else if i.modifiers.shift
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(
|
||||||
|
last_track.id,
|
||||||
|
path.clone(),
|
||||||
|
true,
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
actions.push(FileAction::SetSelected(path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
file_button_response.context_menu(|ui| {
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_BOLT.codepoint,
|
||||||
|
t!("gui.context.files.play_solo")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
actions.push(FileAction::SetSelected(path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_ADD.codepoint,
|
||||||
|
t!("gui.context.files.add_new")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
actions.push(FileAction::SetSelected(path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_SWAP_HORIZ.codepoint,
|
||||||
|
t!("gui.context.files.replace_last")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
|
||||||
|
actions.push(FileAction::SetSelected(path.clone()));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_BROWSER.codepoint,
|
||||||
|
t!("gui.context.files.show_in_fm")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Err(e) = opener::reveal(&path)
|
||||||
|
{
|
||||||
|
eprintln!("Failed to open file manager: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_KEYBOARD.codepoint,
|
||||||
|
t!("gui.context.files.asign_hotkey")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::AssignHotkey(path.clone()));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn draw_footer(&mut self, ui: &mut Ui) {
|
fn draw_footer(&mut self, ui: &mut Ui) {
|
||||||
|
|||||||
+16
-5
@@ -21,6 +21,7 @@ use pwsp::{
|
|||||||
};
|
};
|
||||||
use rfd::FileDialog;
|
use rfd::FileDialog;
|
||||||
use std::{
|
use std::{
|
||||||
|
cmp::Ordering,
|
||||||
fs,
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
@@ -110,14 +111,14 @@ impl SoundpadGui {
|
|||||||
self.app_state.current_dir = Some(path.clone());
|
self.app_state.current_dir = Some(path.clone());
|
||||||
match path.read_dir() {
|
match path.read_dir() {
|
||||||
Ok(read_dir) => {
|
Ok(read_dir) => {
|
||||||
self.app_state.files = read_dir
|
self.app_state.listed_files = read_dir
|
||||||
.filter_map(|res| res.ok())
|
.filter_map(|res| res.ok())
|
||||||
.map(|entry| entry.path())
|
.map(|entry| entry.path())
|
||||||
.collect();
|
.collect();
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("Failed to read directory {:?}: {}", path, e);
|
eprintln!("Failed to read directory {:?}: {}", path, e);
|
||||||
self.app_state.files.clear();
|
self.app_state.listed_files.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -157,8 +158,18 @@ impl SoundpadGui {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
|
||||||
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
|
let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
|
||||||
files.sort();
|
files.sort_by(|a, b| {
|
||||||
|
let a_is_dir = a.is_dir();
|
||||||
|
let b_is_dir = b.is_dir();
|
||||||
|
if a_is_dir && !b_is_dir {
|
||||||
|
Ordering::Less
|
||||||
|
} else if !a_is_dir && b_is_dir {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
a.cmp(b)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let search_query = self.app_state.search_query.to_lowercase();
|
let search_query = self.app_state.search_query.to_lowercase();
|
||||||
let search_query = search_query.trim();
|
let search_query = search_query.trim();
|
||||||
@@ -167,7 +178,7 @@ impl SoundpadGui {
|
|||||||
.into_iter()
|
.into_iter()
|
||||||
.filter(|entry_path| {
|
.filter(|entry_path| {
|
||||||
if entry_path.is_dir() {
|
if entry_path.is_dir() {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !SUPPORTED_EXTENSIONS.contains(
|
if !SUPPORTED_EXTENSIONS.contains(
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ impl App for SoundpadGui {
|
|||||||
&& current_dir == &path
|
&& current_dir == &path
|
||||||
{
|
{
|
||||||
self.app_state.current_dir = None;
|
self.app_state.current_dir = None;
|
||||||
self.app_state.files.clear();
|
self.app_state.listed_files.clear();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+6
-2
@@ -44,14 +44,18 @@ pub struct AppState {
|
|||||||
pub dirs_to_remove: HashSet<PathBuf>,
|
pub dirs_to_remove: HashSet<PathBuf>,
|
||||||
|
|
||||||
pub selected_file: Option<PathBuf>,
|
pub selected_file: Option<PathBuf>,
|
||||||
pub files: HashSet<PathBuf>,
|
pub listed_files: HashSet<PathBuf>,
|
||||||
|
pub listed_dirs: HashSet<PathBuf>,
|
||||||
|
pub dir_cache: HashMap<PathBuf, Vec<PathBuf>>,
|
||||||
|
|
||||||
pub show_hotkeys: bool,
|
pub show_hotkeys: bool,
|
||||||
|
pub hotkey_capture_active: bool,
|
||||||
|
|
||||||
pub hotkey_config: HotkeyConfig,
|
pub hotkey_config: HotkeyConfig,
|
||||||
pub hotkey_search_query: String,
|
pub hotkey_search_query: String,
|
||||||
|
|
||||||
pub assigning_hotkey_slot: Option<String>,
|
pub assigning_hotkey_slot: Option<String>,
|
||||||
pub assigning_hotkey_for_file: Option<PathBuf>,
|
pub assigning_hotkey_for_file: Option<PathBuf>,
|
||||||
pub hotkey_capture_active: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Debug, Clone)]
|
#[derive(Default, Debug, Clone)]
|
||||||
|
|||||||
Reference in New Issue
Block a user