diff --git a/pwsp-gui/locales/app.toml b/pwsp-gui/locales/app.toml index f095144..18858ab 100644 --- a/pwsp-gui/locales/app.toml +++ b/pwsp-gui/locales/app.toml @@ -70,6 +70,61 @@ kz = "Жою" he = "הסר" 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] en = "Play Solo" ru = "Играть" diff --git a/pwsp-gui/src/gui/mod.rs b/pwsp-gui/src/gui/mod.rs index 6e588d4..98832d0 100644 --- a/pwsp-gui/src/gui/mod.rs +++ b/pwsp-gui/src/gui/mod.rs @@ -159,6 +159,13 @@ impl SoundpadGui { pub fn get_filtered_files(&self) -> Vec { let mut files: Vec = 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| { let a_is_dir = a.is_dir(); let b_is_dir = b.is_dir(); @@ -167,7 +174,7 @@ impl SoundpadGui { } else if !a_is_dir && b_is_dir { Ordering::Greater } else { - a.cmp(b) + sort_order.compare(a, b) } }); @@ -334,5 +341,19 @@ mod tests { let filtered_search = gui.get_filtered_files(); assert_eq!(filtered_search.len(), 1); 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); } } diff --git a/pwsp-gui/src/gui/views/body.rs b/pwsp-gui/src/gui/views/body.rs index ab62a2c..ffd2077 100644 --- a/pwsp-gui/src/gui/views/body.rs +++ b/pwsp-gui/src/gui/views/body.rs @@ -5,7 +5,10 @@ use egui::{ }; use egui_dnd::dnd; 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 std::{cmp::Ordering, path::Path, path::PathBuf}; @@ -135,6 +138,65 @@ impl SoundpadGui { { 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( ui, entry_path, + &self.config, &mut self.app_state, &self.audio_player_state, &mut actions, @@ -226,6 +289,7 @@ impl SoundpadGui { fn draw_tree_node_dir( ui: &mut Ui, path: std::path::PathBuf, + config: &GuiConfig, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, @@ -247,6 +311,7 @@ impl SoundpadGui { read.push(entry.path()); } } + let sort_order = config.get_sort_order(&path); read.sort_by(|a, b| { let a_is_dir = a.is_dir(); let b_is_dir = b.is_dir(); @@ -255,7 +320,7 @@ impl SoundpadGui { } else if !a_is_dir && b_is_dir { Ordering::Greater } else { - a.cmp(b) + sort_order.compare(a, b) } }); 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( ui: &mut Ui, path: std::path::PathBuf, + config: &GuiConfig, app_state: &mut AppState, audio_player_state: &AudioPlayerState, actions: &mut Vec, ) { 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 { Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions); } diff --git a/pwsp-lib/src/types/config.rs b/pwsp-lib/src/types/config.rs index b8c4edd..9668237 100644 --- a/pwsp-lib/src/types/config.rs +++ b/pwsp-lib/src/types/config.rs @@ -4,7 +4,13 @@ use crate::{ }; use anyhow::Result; 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)] #[serde(default)] @@ -45,6 +51,21 @@ pub enum PreferredTheme { 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)] #[serde(default)] pub struct GuiConfig { @@ -57,10 +78,38 @@ pub struct GuiConfig { pub pause_on_exit: bool, pub dirs: Vec, + pub dirs_settings: HashMap, 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 { fn default() -> Self { GuiConfig { @@ -75,11 +124,23 @@ impl Default for GuiConfig { dirs: vec![ensure_pwsp_audio_dir()], preferred_theme: PreferredTheme::System, + dirs_settings: HashMap::new(), } } } 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<()> { let config_path = get_config_path()?.join("gui.json");