mod draw;
mod input;
mod update;
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp::{
types::{
audio_player::PlayerState,
config::GuiConfig,
config::HotkeyConfig,
gui::{AppState, AudioPlayerState},
socket::Request,
},
utils::{
daemon::get_daemon_config,
gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread},
},
};
use rfd::FileDialog;
use std::{
error::Error,
path::PathBuf,
sync::{Arc, Mutex},
};
const SUPPORTED_EXTENSIONS: [&str; 12] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "mka", "webm", "avi",
];
struct SoundpadGui {
pub app_state: AppState,
pub config: GuiConfig,
pub audio_player_state: AudioPlayerState,
pub audio_player_state_shared: Arc<Mutex<AudioPlayerState>>,
}
impl SoundpadGui {
fn new(ctx: &Context) -> Self {
let audio_player_state = Arc::new(Mutex::new(AudioPlayerState::default()));
start_app_state_thread(audio_player_state.clone());
let config = get_gui_config();
ctx.set_zoom_factor(config.scale_factor);
let mut soundpad_gui = SoundpadGui {
app_state: AppState::default(),
config: config.clone(),
audio_player_state: AudioPlayerState::default(),
audio_player_state_shared: audio_player_state.clone(),
};
soundpad_gui.app_state.dirs = config.dirs;
soundpad_gui.app_state.hotkey_config = HotkeyConfig::load().unwrap_or_default();
soundpad_gui
}
pub fn play_toggle(&mut self) {
let (new_state, request) = {
let guard = self
.audio_player_state_shared
.lock()
.unwrap_or_else(|e| e.into_inner());
match guard.state {
PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause(None))),
PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume(None))),
PlayerState::Stopped => (None, None),
}
};
if let Some(req) = request {
make_request_async(req);
}
if let Some(state) = new_state {
let mut guard = self
.audio_player_state_shared
.lock()
.unwrap_or_else(|e| e.into_inner());
guard.new_state = Some(state.clone());
guard.state = state;
}
}
pub fn open_file(&mut self) {
let file_dialog = FileDialog::new().add_filter("Audio File", &SUPPORTED_EXTENSIONS);
if let Some(path) = file_dialog.pick_file() {
self.play_file(&path, false);
}
}
pub fn add_dirs(&mut self) {
let file_dialog = FileDialog::new();
if let Some(paths) = file_dialog.pick_folders() {
for path in paths {
self.app_state.dirs.push(path);
}
self.app_state.dirs = self.app_state.dirs.iter().unique().cloned().collect();
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
}
pub fn open_dir(&mut self, path: &PathBuf) {
self.app_state.current_dir = Some(path.clone());
match path.read_dir() {
Ok(read_dir) => {
self.app_state.files = read_dir
.filter_map(|res| res.ok())
.map(|entry| entry.path())
.collect();
}
Err(e) => {
eprintln!("Failed to read directory {:?}: {}", path, e);
self.app_state.files.clear();
}
}
}
pub fn play_file(&mut self, path: &PathBuf, concurrent: bool) {
make_request_async(Request::play(&path.to_string_lossy(), concurrent));
}
pub fn set_input(&mut self, name: String) {
make_request_async(Request::set_input(&name));
if self.config.save_input {
let mut daemon_config = get_daemon_config();
daemon_config.default_input_name = Some(name);
daemon_config.save_to_file().ok();
}
}
pub fn toggle_loop(&mut self, id: Option<u32>) {
make_request_async(Request::toggle_loop(id));
}
pub fn pause(&mut self, id: Option<u32>) {
make_request_async(Request::pause(id));
}
pub fn resume(&mut self, id: Option<u32>) {
make_request_async(Request::resume(id));
}
pub fn stop(&mut self, id: Option<u32>) {
make_request_async(Request::stop(id));
}
pub fn play_hotkey_slot(&mut self, slot: &str) {
make_request_async(Request::play_hotkey(slot));
}
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
files.sort();
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
files
.into_iter()
.filter(|entry_path| {
if entry_path.is_dir() {
return false;
}
if !SUPPORTED_EXTENSIONS.contains(
&entry_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
return false;
}
if !search_query.is_empty() {
let file_name = entry_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
return false;
}
}
true
})
.collect()
}
}
pub async fn run() -> Result<(), Box<dyn Error>> {
const ICON: &[u8] = include_bytes!("../../assets/icon.png");
let options = NativeOptions {
vsync: true,
centered: true,
hardware_acceleration: HardwareAcceleration::Preferred,
viewport: ViewportBuilder::default()
.with_app_id("ru.arabianq.pwsp")
.with_inner_size(Vec2::new(1200.0, 800.0))
.with_min_inner_size(Vec2::new(800.0, 600.0))
.with_icon(from_png_bytes(ICON)?),
..Default::default()
};
match run_native(
"Pipewire Soundpad",
options,
Box::new(|cc| {
egui_material_icons::initialize(&cc.egui_ctx);
Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
}),
) {
Ok(_) => {
let config = get_gui_config();
if config.pause_on_exit {
make_request_sync(Request::pause(None)).ok();
}
Ok(())
}
Err(e) => Err(e.into()),
}
}