feat(gui): sorting options (#134)

* feat(gui): added an ability to copy ```pwsp-cli action play``` command for every sound

* feat(gui): added files sorting options
This commit is contained in:
Tarasov Aleksandr
2026-06-04 20:14:28 +03:00
committed by GitHub
parent c173e602ad
commit 410a2c7959
4 changed files with 209 additions and 6 deletions
+55
View File
@@ -70,6 +70,61 @@ kz = "Жою"
he = "הסר" he = "הסר"
pt-BR = "Remover" pt-BR = "Remover"
[gui.context.dirs.sort_by]
en = "Sort by"
ru = "Сортировка"
es = "Ordenar por"
fr = "Trier par"
zh = "排序方式"
ar = "ترتيب حسب"
kz = "Сұрыптау"
he = "מיין לפי"
pt-BR = "Ordenar por"
[gui.sort.alpha_asc]
en = "Alphabetical (A-Z)"
ru = "По алфавиту (А-Я)"
es = "Alfabético (A-Z)"
fr = "Alphabétique (A-Z)"
zh = "字母顺序 (A-Z)"
ar = "أبجدي (A-Z)"
kz = "Әліпби бойынша (А-Я)"
he = "אלפביתי (A-Z)"
pt-BR = "Alfabético (A-Z)"
[gui.sort.alpha_desc]
en = "Alphabetical (Z-A)"
ru = "По алфавиту (Я-А)"
es = "Alfabético (Z-A)"
fr = "Alphabétique (Z-A)"
zh = "字母顺序 (Z-A)"
ar = "أبجدي (Z-A)"
kz = "Әліпби бойынша (Я-А)"
he = "אלפביתי (Z-A)"
pt-BR = "Alfabético (Z-A)"
[gui.sort.date_newest]
en = "Date modified (Newest first)"
ru = "Дата изменения (Сначала новые)"
es = "Fecha de modificación (Más nuevo primero)"
fr = "Date de modification (Plus récent en premier)"
zh = "修改日期 (最新优先)"
ar = "تاريخ التعديل (الأحدث أولاً)"
kz = "Өзгертілген күні (Жаңалары бірінші)"
he = "תאריך שינוי (החדש ביותר ראשון)"
pt-BR = "Data de modificação (Mais novo primeiro)"
[gui.sort.date_oldest]
en = "Date modified (Oldest first)"
ru = "Дата изменения (Сначала старые)"
es = "Fecha de modificación (Más antiguo primero)"
fr = "Date de modification (Plus ancien en premier)"
zh = "修改日期 (最旧优先)"
ar = "تاريخ التعديل (الأقدم أولاً)"
kz = "Өзгертілген күні (Ескілері бірінші)"
he = "תאריך שינוי (הישן ביותר ראשון)"
pt-BR = "Data de modificação (Mais antigo primeiro)"
[gui.context.files.play_solo] [gui.context.files.play_solo]
en = "Play Solo" en = "Play Solo"
ru = "Играть" ru = "Играть"
+22 -1
View File
@@ -159,6 +159,13 @@ 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.listed_files.iter().cloned().collect(); let mut files: Vec<PathBuf> = self.app_state.listed_files.iter().cloned().collect();
let sort_order = self
.app_state
.current_dir
.as_ref()
.map(|d| self.config.get_sort_order(d))
.unwrap_or_default();
files.sort_by(|a, b| { files.sort_by(|a, b| {
let a_is_dir = a.is_dir(); let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir(); let b_is_dir = b.is_dir();
@@ -167,7 +174,7 @@ impl SoundpadGui {
} else if !a_is_dir && b_is_dir { } else if !a_is_dir && b_is_dir {
Ordering::Greater Ordering::Greater
} else { } else {
a.cmp(b) sort_order.compare(a, b)
} }
}); });
@@ -334,5 +341,19 @@ mod tests {
let filtered_search = gui.get_filtered_files(); let filtered_search = gui.get_filtered_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);
// Test sort order descending
gui.app_state.current_dir = Some(PathBuf::from("dummy_dir"));
gui.config.dirs_settings.insert(
PathBuf::from("dummy_dir"),
pwsp_lib::types::config::DirSettings {
sort_order: pwsp_lib::types::config::SortOrder::AlphabeticalDesc,
},
);
gui.app_state.search_query = String::new();
let filtered_desc = gui.get_filtered_files();
assert_eq!(filtered_desc.len(), 2);
assert_eq!(filtered_desc[0], file_c);
assert_eq!(filtered_desc[1], file_b);
} }
} }
+70 -4
View File
@@ -5,7 +5,10 @@ use egui::{
}; };
use egui_dnd::dnd; use egui_dnd::dnd;
use egui_material_icons::icons::*; use egui_material_icons::icons::*;
use pwsp_lib::types::{gui::AppState, gui::AudioPlayerState}; use pwsp_lib::types::{
config::{GuiConfig, SortOrder},
gui::{AppState, AudioPlayerState},
};
use rust_i18n::t; use rust_i18n::t;
use std::{cmp::Ordering, path::Path, path::PathBuf}; use std::{cmp::Ordering, path::Path, path::PathBuf};
@@ -135,6 +138,65 @@ impl SoundpadGui {
{ {
self.app_state.dirs_to_remove.insert(path.clone()); self.app_state.dirs_to_remove.insert(path.clone());
} }
ui.separator();
ui.label(t!("gui.context.dirs.sort_by"));
let current_order = self
.config
.dirs_settings
.get(path)
.map(|s| s.sort_order)
.unwrap_or_default();
let mut new_order = None;
if ui
.radio(
current_order == SortOrder::AlphabeticalAsc,
t!("gui.sort.alpha_asc"),
)
.clicked()
{
new_order = Some(SortOrder::AlphabeticalAsc);
}
if ui
.radio(
current_order == SortOrder::AlphabeticalDesc,
t!("gui.sort.alpha_desc"),
)
.clicked()
{
new_order = Some(SortOrder::AlphabeticalDesc);
}
if ui
.radio(
current_order == SortOrder::DateModifiedNewest,
t!("gui.sort.date_newest"),
)
.clicked()
{
new_order = Some(SortOrder::DateModifiedNewest);
}
if ui
.radio(
current_order == SortOrder::DateModifiedOldest,
t!("gui.sort.date_oldest"),
)
.clicked()
{
new_order = Some(SortOrder::DateModifiedOldest);
}
if let Some(order) = new_order {
self.config
.dirs_settings
.entry(path.clone())
.or_default()
.sort_order = order;
self.config.save_to_file().ok();
self.app_state.dir_cache.remove(path);
self.open_dir(path);
}
}); });
}); });
}); });
@@ -192,6 +254,7 @@ impl SoundpadGui {
Self::draw_tree_node( Self::draw_tree_node(
ui, ui,
entry_path, entry_path,
&self.config,
&mut self.app_state, &mut self.app_state,
&self.audio_player_state, &self.audio_player_state,
&mut actions, &mut actions,
@@ -226,6 +289,7 @@ impl SoundpadGui {
fn draw_tree_node_dir( fn draw_tree_node_dir(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
config: &GuiConfig,
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
@@ -247,6 +311,7 @@ impl SoundpadGui {
read.push(entry.path()); read.push(entry.path());
} }
} }
let sort_order = config.get_sort_order(&path);
read.sort_by(|a, b| { read.sort_by(|a, b| {
let a_is_dir = a.is_dir(); let a_is_dir = a.is_dir();
let b_is_dir = b.is_dir(); let b_is_dir = b.is_dir();
@@ -255,7 +320,7 @@ impl SoundpadGui {
} else if !a_is_dir && b_is_dir { } else if !a_is_dir && b_is_dir {
Ordering::Greater Ordering::Greater
} else { } else {
a.cmp(b) sort_order.compare(a, b)
} }
}); });
app_state.dir_cache.insert(path.clone(), read.clone()); app_state.dir_cache.insert(path.clone(), read.clone());
@@ -287,7 +352,7 @@ impl SoundpadGui {
} }
} }
} }
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions); Self::draw_tree_node(ui, child, config, app_state, audio_player_state, actions);
} }
}); });
} }
@@ -437,12 +502,13 @@ impl SoundpadGui {
fn draw_tree_node( fn draw_tree_node(
ui: &mut Ui, ui: &mut Ui,
path: std::path::PathBuf, path: std::path::PathBuf,
config: &GuiConfig,
app_state: &mut AppState, app_state: &mut AppState,
audio_player_state: &AudioPlayerState, audio_player_state: &AudioPlayerState,
actions: &mut Vec<FileAction>, actions: &mut Vec<FileAction>,
) { ) {
if path.is_dir() { if path.is_dir() {
Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions); Self::draw_tree_node_dir(ui, path, config, app_state, audio_player_state, actions);
} 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);
} }
+62 -1
View File
@@ -4,7 +4,13 @@ use crate::{
}; };
use anyhow::Result; use anyhow::Result;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fs, path::PathBuf}; use std::{
cmp::Ordering,
collections::HashMap,
fs,
path::{Path, PathBuf},
time::SystemTime,
};
#[derive(Default, Clone, Serialize, Deserialize)] #[derive(Default, Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
@@ -45,6 +51,21 @@ pub enum PreferredTheme {
Dark, Dark,
} }
#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
pub enum SortOrder {
#[default]
AlphabeticalAsc,
AlphabeticalDesc,
DateModifiedNewest,
DateModifiedOldest,
}
#[derive(Default, Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(default)]
pub struct DirSettings {
pub sort_order: SortOrder,
}
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
#[serde(default)] #[serde(default)]
pub struct GuiConfig { pub struct GuiConfig {
@@ -57,10 +78,38 @@ pub struct GuiConfig {
pub pause_on_exit: bool, pub pause_on_exit: bool,
pub dirs: Vec<PathBuf>, pub dirs: Vec<PathBuf>,
pub dirs_settings: HashMap<PathBuf, DirSettings>,
pub preferred_theme: PreferredTheme, pub preferred_theme: PreferredTheme,
} }
impl SortOrder {
pub fn compare(&self, a: &Path, b: &Path) -> Ordering {
match self {
SortOrder::AlphabeticalAsc => a.cmp(b),
SortOrder::AlphabeticalDesc => b.cmp(a),
SortOrder::DateModifiedNewest => {
let a_time = fs::metadata(a)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
let b_time = fs::metadata(b)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
b_time.cmp(&a_time)
}
SortOrder::DateModifiedOldest => {
let a_time = fs::metadata(a)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
let b_time = fs::metadata(b)
.and_then(|m| m.modified())
.unwrap_or(SystemTime::UNIX_EPOCH);
a_time.cmp(&b_time)
}
}
}
}
impl Default for GuiConfig { impl Default for GuiConfig {
fn default() -> Self { fn default() -> Self {
GuiConfig { GuiConfig {
@@ -75,11 +124,23 @@ impl Default for GuiConfig {
dirs: vec![ensure_pwsp_audio_dir()], dirs: vec![ensure_pwsp_audio_dir()],
preferred_theme: PreferredTheme::System, preferred_theme: PreferredTheme::System,
dirs_settings: HashMap::new(),
} }
} }
} }
impl GuiConfig { impl GuiConfig {
pub fn get_sort_order(&self, path: &Path) -> SortOrder {
let mut current = Some(path);
while let Some(p) = current {
if let Some(settings) = self.dirs_settings.get(p) {
return settings.sort_order;
}
current = p.parent();
}
SortOrder::default()
}
pub fn save_to_file(&mut self) -> Result<()> { pub fn save_to_file(&mut self) -> Result<()> {
let config_path = get_config_path()?.join("gui.json"); let config_path = get_config_path()?.join("gui.json");