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, 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; 11] = [ "mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "webm", "avi", ]; struct SoundpadGui { pub app_state: AppState, pub config: GuiConfig, pub audio_player_state: AudioPlayerState, pub audio_player_state_shared: Arc>, } 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 } pub fn play_toggle(&mut self) { let (new_state, request) = { let guard = self.audio_player_state_shared.lock().unwrap(); 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(); 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_str().unwrap(), 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) { make_request_async(Request::toggle_loop(id)); } pub fn pause(&mut self, id: Option) { make_request_async(Request::pause(id)); } pub fn resume(&mut self, id: Option) { make_request_async(Request::resume(id)); } pub fn stop(&mut self, id: Option) { make_request_async(Request::stop(id)); } pub fn get_filtered_files(&self) -> Vec { let mut files: Vec = 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> { 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()), } }