diff --git a/README.md b/README.md index 1e50bac..bb1ce40 100644 --- a/README.md +++ b/README.md @@ -173,6 +173,29 @@ pwsp-cli --help pwsp-cli set position 20 ``` +### **Hotkeys & Controls** + +#### **Keyboard Shortcuts** + +| Key | Action | +| :----------------------- | :--------------------------------------------------- | +| **Esc** | Close application | +| **Space** | Pause / Resume audio | +| **Backspace** | Stop all audio tracks | +| **Enter** | Play selected file (stops all other tracks) | +| **Ctrl + Enter** | Add selected file to playback (plays simultaneously) | +| **Shift + Enter** | Replace the last added track with the selected one | +| **I** | Open / Close settings | +| **/** | Focus search field | +| **Ctrl + ↑ / ↓** | Navigate through files | +| **Ctrl + Shift + ↑ / ↓** | Navigate through directories | + +#### **Mouse Controls** + +* **Left Click**: Play track (stops all other tracks). +* **Ctrl + Left Click**: Add track (plays simultaneously with current tracks). +* **Shift + Left Click**: Replace the last added track with the selected one. + # **🤝 Contributing** Contributions are welcome\! If you have ideas for improvements or find a bug, feel free to create diff --git a/assets/screenshot.png b/assets/screenshot.png index 1068a9f..265d225 100644 Binary files a/assets/screenshot.png and b/assets/screenshot.png differ diff --git a/src/bin/cli.rs b/src/bin/cli.rs index 0caed1b..4037368 100644 --- a/src/bin/cli.rs +++ b/src/bin/cli.rs @@ -36,17 +36,36 @@ enum Actions { /// Ping the daemon Ping, /// Pause audio playback - Pause, + Pause { + #[clap(short, long)] + id: Option, + }, /// Resume audio playback - Resume, + Resume { + #[clap(short, long)] + id: Option, + }, /// Toggle pause - TogglePause, + TogglePause { + #[clap(short, long)] + id: Option, + }, /// Stop audio playback and clear the queue - Stop, + Stop { + #[clap(short, long)] + id: Option, + }, /// Play a file - Play { file_path: PathBuf }, + Play { + file_path: PathBuf, + #[clap(short, long)] + concurrent: bool, + }, /// Toggle loop - ToggleLoop, + ToggleLoop { + #[clap(short, long)] + id: Option, + }, } #[derive(Subcommand, Debug)] @@ -56,31 +75,47 @@ enum GetCommands { /// Playback volume Volume, /// Playback position (in seconds) - Position, + Position { + #[clap(short, long)] + id: Option, + }, /// Duration of the current file - Duration, + Duration { + #[clap(short, long)] + id: Option, + }, /// Player state (Playing, Paused or Stopped) State, - /// Current playing file path - CurrentFilePath, + /// Get all playing tracks + Tracks, /// Current audio input Input, /// All audio inputs Inputs, - /// Is loop enabled (true or false) - Loop, } #[derive(Subcommand, Debug)] enum SetCommands { /// Playback volume - Volume { volume: f32 }, + Volume { + volume: f32, + #[clap(short, long)] + id: Option, + }, /// Playback position (in seconds) - Position { position: f32 }, + Position { + position: f32, + #[clap(short, long)] + id: Option, + }, /// Audio input id (see pwsp-cli get inputs) Input { name: String }, /// Enable or disable loop (true or false) - Loop { enabled: String }, + Loop { + enabled: String, + #[clap(short, long)] + id: Option, + }, } #[tokio::main] @@ -92,29 +127,31 @@ async fn main() -> Result<(), Box> { let request = match cli.command { Commands::Action { action } => match action { Actions::Ping => Request::ping(), - Actions::Pause => Request::pause(), - Actions::Resume => Request::resume(), - Actions::TogglePause => Request::toggle_pause(), - Actions::Stop => Request::stop(), - Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()), - Actions::ToggleLoop => Request::toggle_loop(), + Actions::Pause { id } => Request::pause(id), + Actions::Resume { id } => Request::resume(id), + Actions::TogglePause { id } => Request::toggle_pause(id), + Actions::Stop { id } => Request::stop(id), + Actions::Play { + file_path, + concurrent, + } => Request::play(file_path.to_str().unwrap(), concurrent), + Actions::ToggleLoop { id } => Request::toggle_loop(id), }, Commands::Get { parameter } => match parameter { GetCommands::IsPaused => Request::get_is_paused(), GetCommands::Volume => Request::get_volume(), - GetCommands::Position => Request::get_position(), - GetCommands::Duration => Request::get_duration(), + GetCommands::Position { id } => Request::get_position(id), + GetCommands::Duration { id } => Request::get_duration(id), GetCommands::State => Request::get_state(), - GetCommands::CurrentFilePath => Request::get_current_file_path(), + GetCommands::Tracks => Request::get_tracks(), GetCommands::Input => Request::get_input(), GetCommands::Inputs => Request::get_inputs(), - GetCommands::Loop => Request::get_loop(), }, Commands::Set { parameter } => match parameter { - SetCommands::Volume { volume } => Request::set_volume(volume), - SetCommands::Position { position } => Request::seek(position), + SetCommands::Volume { volume, id } => Request::set_volume(volume, id), + SetCommands::Position { position, id } => Request::seek(position, id), SetCommands::Input { name } => Request::set_input(&name), - SetCommands::Loop { enabled } => Request::set_loop(&enabled), + SetCommands::Loop { enabled, id } => Request::set_loop(&enabled, id), }, }; diff --git a/src/bin/daemon.rs b/src/bin/daemon.rs index 60a6880..af70969 100644 --- a/src/bin/daemon.rs +++ b/src/bin/daemon.rs @@ -1,8 +1,5 @@ use pwsp::{ - types::{ - audio_player::PlayerState, - socket::{Request, Response}, - }, + types::socket::{Request, Response}, utils::{ commands::parse_command, daemon::{ @@ -122,18 +119,7 @@ async fn player_loop() { loop { let mut audio_player = get_audio_player().await.lock().await; - // Start playback again if loop is enabled - let should_play = audio_player.get_state() == PlayerState::Stopped - && audio_player.current_file_path.is_some() - && audio_player.looped; - - if should_play { - let file_path = audio_player.current_file_path.clone().unwrap(); - audio_player - .play(&file_path) - .await - .expect("Something went wrong while trying to play the file"); - } + audio_player.update().await; sleep(Duration::from_millis(100)).await; } diff --git a/src/gui/draw.rs b/src/gui/draw.rs index 738d982..c7f51c2 100644 --- a/src/gui/draw.rs +++ b/src/gui/draw.rs @@ -4,11 +4,32 @@ use egui::{ Slider, TextEdit, Ui, Vec2, }; use egui_material_icons::icons; -use pwsp::types::audio_player::PlayerState; +use pwsp::types::audio_player::TrackInfo; use pwsp::utils::gui::format_time_pair; -use std::{error::Error, path::PathBuf}; +use std::{error::Error, path::PathBuf, time::Instant}; + +use pwsp::types::gui::AppState; + +enum TrackAction { + Pause(u32), + Resume(u32), + ToggleLoop(u32), + Stop(u32), +} impl SoundpadGui { + fn get_volume_icon(volume: f32) -> &'static str { + if volume > 0.7 { + icons::ICON_VOLUME_UP + } else if volume <= 0.0 { + icons::ICON_VOLUME_OFF + } else if volume < 0.3 { + icons::ICON_VOLUME_MUTE + } else { + icons::ICON_VOLUME_DOWN + } + } + pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) { ui.centered_and_justified(|ui| { ui.label( @@ -74,11 +95,24 @@ impl SoundpadGui { fn draw_header(&mut self, ui: &mut Ui) { ui.vertical_centered_justified(|ui| { - // Current file name + self.draw_controls(ui); + }); + } + + fn draw_controls(&mut self, ui: &mut Ui) { + if self.audio_player_state.tracks.is_empty() { + ui.label("No tracks playing"); + return; + } + + let tracks = self.audio_player_state.tracks.clone(); + let mut action = None; + + for track in tracks { ui.label( RichText::new( - self.audio_player_state - .current_file_path + track + .path .file_stem() .unwrap_or_default() .to_str() @@ -87,32 +121,76 @@ impl SoundpadGui { .color(Color32::WHITE) .family(FontFamily::Monospace), ); - // Media controls - self.draw_controls(ui); + if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) { + action = Some(act); + } ui.separator(); - }); + } + + if let Some(action) = action { + match action { + TrackAction::Pause(id) => self.pause(Some(id)), + TrackAction::Resume(id) => self.resume(Some(id)), + TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)), + TrackAction::Stop(id) => self.stop(Some(id)), + } + } } - fn draw_controls(&mut self, ui: &mut Ui) { + fn draw_track_control( + ui: &mut Ui, + app_state: &mut AppState, + track: &TrackInfo, + ) -> Option { + let ui_state = app_state.track_ui_states.entry(track.id).or_default(); + + let should_update_position = !ui_state.position_dragged + && ui_state + .ignore_position_update_until + .map(|t| Instant::now() > t) + .unwrap_or(true); + + if should_update_position { + ui_state.position_slider_value = track.position; + } + + let should_update_volume = !ui_state.volume_dragged + && ui_state + .ignore_volume_update_until + .map(|t| Instant::now() > t) + .unwrap_or(true); + + if should_update_volume { + ui_state.volume_slider_value = track.volume; + } + + let mut action = None; + ui.horizontal_top(|ui| { // ---------- Play Button ---------- - let play_button = Button::new(match self.audio_player_state.state { - PlayerState::Playing => icons::ICON_PAUSE, - PlayerState::Paused | PlayerState::Stopped => icons::ICON_PLAY_ARROW, + let play_button = Button::new(if track.paused { + icons::ICON_PLAY_ARROW + } else { + icons::ICON_PAUSE }) .corner_radius(15.0); let play_button_response = ui.add_sized([30.0, 30.0], play_button); if play_button_response.clicked() { - self.play_toggle(); + if track.paused { + action = Some(TrackAction::Resume(track.id)); + } else { + action = Some(TrackAction::Pause(track.id)); + } } // -------------------------------- // ---------- Loop Button ---------- let loop_button = Button::new( - RichText::new(match self.audio_player_state.looped { - true => icons::ICON_REPEAT_ONE, - false => icons::ICON_REPEAT, + RichText::new(if track.looped { + icons::ICON_REPEAT_ONE + } else { + icons::ICON_REPEAT }) .size(18.0), ) @@ -120,17 +198,15 @@ impl SoundpadGui { let loop_button_response = ui.add_sized([15.0, 30.0], loop_button); if loop_button_response.clicked() { - self.toggle_loop(); + action = Some(TrackAction::ToggleLoop(track.id)); } // -------------------------------- // ---------- Position Slider ---------- - let position_slider = Slider::new( - &mut self.app_state.position_slider_value, - 0.0..=self.audio_player_state.duration, - ) - .show_value(false) - .step_by(0.01); + let duration = track.duration.unwrap_or(1.0); + let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration) + .show_value(false) + .step_by(0.01); let default_slider_width = ui.spacing().slider_width; let position_slider_width = ui.available_width() @@ -140,49 +216,47 @@ impl SoundpadGui { ui.spacing_mut().slider_width = position_slider_width; let position_slider_response = ui.add_sized([30.0, 30.0], position_slider); if position_slider_response.drag_stopped() { - self.app_state.position_dragged = true; + ui_state.position_dragged = true; } // -------------------------------- // ---------- Time Label ---------- - let time_label = Label::new( - RichText::new(format_time_pair( - self.audio_player_state.position, - self.audio_player_state.duration, - )) - .monospace(), - ); + let time_label = + Label::new(RichText::new(format_time_pair(track.position, duration)).monospace()); ui.add_sized([30.0, 30.0], time_label); // -------------------------------- // ---------- Volume Icon ---------- - let volume_icon = if self.audio_player_state.volume > 0.7 { - icons::ICON_VOLUME_UP - } else if self.audio_player_state.volume == 0.0 { - icons::ICON_VOLUME_OFF - } else if self.audio_player_state.volume < 0.3 { - icons::ICON_VOLUME_MUTE - } else { - icons::ICON_VOLUME_DOWN - }; - let volume_icon = Label::new(RichText::new(volume_icon).size(18.0)); - ui.add_sized([30.0, 30.0], volume_icon); + let volume_icon = Self::get_volume_icon(track.volume); + let volume_label = Label::new(RichText::new(volume_icon).size(18.0)); + ui.add_sized([30.0, 30.0], volume_label) + .on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0)); // -------------------------------- // ---------- Volume Slider ---------- - let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0) + let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0) .show_value(false) .step_by(0.01); - ui.spacing_mut().slider_width = default_slider_width; + ui.spacing_mut().slider_width = default_slider_width - 30.0; ui.spacing_mut().item_spacing.x = 0.0; let volume_slider_response = ui.add_sized([30.0, 30.0], volume_slider); if volume_slider_response.drag_stopped() { - self.app_state.volume_dragged = true; + ui_state.volume_dragged = true; + } + // -------------------------------- + + // ---------- Stop Button --------- + let stop_button = Button::new(icons::ICON_CLOSE).frame(false); + let stop_button_response = ui.add_sized([30.0, 30.0], stop_button); + if stop_button_response.clicked() { + action = Some(TrackAction::Stop(track.id)); } // -------------------------------- }); + + action } fn draw_body(&mut self, ui: &mut Ui) { @@ -316,7 +390,18 @@ impl SoundpadGui { let file_button = Button::new(file_button_text).frame(false); let file_button_response = ui.add(file_button); if file_button_response.clicked() { - self.play_file(&entry_path); + 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); } } @@ -327,7 +412,7 @@ impl SoundpadGui { fn draw_footer(&mut self, ui: &mut Ui) { ui.add_space(5.0); - ui.horizontal_top(|ui| { + ui.horizontal(|ui| { // ---------- Microphone selection ---------- let mut mics: Vec<(&String, &String)> = self.audio_player_state.all_inputs.iter().collect(); @@ -336,6 +421,7 @@ impl SoundpadGui { let mut selected_input = self.audio_player_state.current_input.to_owned(); let prev_input = selected_input.to_owned(); ComboBox::from_label("Choose microphone") + .height(30.0) .selected_text( self.audio_player_state .all_inputs @@ -353,10 +439,40 @@ impl SoundpadGui { } // -------------------------------- + // ---------- Master Volume Slider ---------- + let volume_icon = Self::get_volume_icon(self.audio_player_state.volume); + let volume_label = Label::new(RichText::new(volume_icon).size(18.0)); + ui.add_sized([18.0, 18.0], volume_label) + .on_hover_text(format!( + "Master Volume: {:.0}%", + self.audio_player_state.volume * 100.0 + )); + + let should_update_volume = !self.app_state.volume_dragged + && self + .app_state + .ignore_volume_update_until + .map(|t| Instant::now() > t) + .unwrap_or(true); + + if should_update_volume { + self.app_state.volume_slider_value = self.audio_player_state.volume; + } + + let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0) + .show_value(false) + .step_by(0.01); + let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider); + if volume_slider_response.drag_stopped() { + self.app_state.volume_dragged = true; + } + // ------------------------------------------ + ui.add_space(ui.available_width() - 18.0 - ui.spacing().item_spacing.x); // ---------- Settings button ---------- - let settings_button = Button::new(icons::ICON_SETTINGS).frame(false); + let settings_button = + Button::new(icons::ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false); let settings_button_response = ui.add_sized([18.0, 18.0], settings_button); if settings_button_response.clicked() { self.app_state.show_settings = true; diff --git a/src/gui/input.rs b/src/gui/input.rs index b871913..dc27399 100644 --- a/src/gui/input.rs +++ b/src/gui/input.rs @@ -21,7 +21,17 @@ impl SoundpadGui { } if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() { - self.play_file(&self.app_state.selected_file.clone().unwrap()); + let path = &self.app_state.selected_file.clone().unwrap(); + if i.modifiers.ctrl { + self.play_file(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(path, true); + } else { + self.play_file(path, false); + } } if !self.app_state.show_settings { @@ -30,6 +40,11 @@ impl SoundpadGui { self.play_toggle(); } + // Stop all audio tracks on backspace + if i.key_pressed(Key::Backspace) { + self.stop(None); + } + // Focus search field if i.key_pressed(Key::Slash) && self.app_state.search_field_id.is_some() { self.app_state.force_focus_id = self.app_state.search_field_id; diff --git a/src/gui/mod.rs b/src/gui/mod.rs index ee00657..5f53f48 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -13,13 +13,13 @@ use pwsp::{ }, utils::{ daemon::get_daemon_config, - gui::{get_gui_config, make_request_sync, start_app_state_thread}, + gui::{get_gui_config, make_request_async, make_request_sync, start_app_state_thread}, }, }; use rfd::FileDialog; -use std::path::PathBuf; use std::{ error::Error, + path::PathBuf, sync::{Arc, Mutex}, }; @@ -59,14 +59,14 @@ impl SoundpadGui { let (new_state, request) = { let guard = self.audio_player_state_shared.lock().unwrap(); match guard.state { - PlayerState::Playing => (Some(PlayerState::Paused), Some(Request::pause())), - PlayerState::Paused => (Some(PlayerState::Playing), Some(Request::resume())), + 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_sync(req).ok(); + make_request_async(req); } if let Some(state) = new_state { @@ -79,7 +79,7 @@ impl SoundpadGui { 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); + self.play_file(&path, false); } } @@ -116,12 +116,12 @@ impl SoundpadGui { .collect(); } - pub fn play_file(&mut self, path: &PathBuf) { - make_request_sync(Request::play(path.to_str().unwrap())).ok(); + 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_sync(Request::set_input(&name)).ok(); + make_request_async(Request::set_input(&name)); if self.config.save_input { let mut daemon_config = get_daemon_config(); @@ -130,8 +130,20 @@ impl SoundpadGui { } } - pub fn toggle_loop(&mut self) { - make_request_sync(Request::toggle_loop()).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)); } } @@ -163,7 +175,7 @@ pub async fn run() -> Result<(), Box> { Ok(_) => { let config = get_gui_config(); if config.pause_on_exit { - make_request_sync(Request::pause()).ok(); + make_request_sync(Request::pause(None)).ok(); } Ok(()) } diff --git a/src/gui/update.rs b/src/gui/update.rs index ac8a83d..faa161f 100644 --- a/src/gui/update.rs +++ b/src/gui/update.rs @@ -3,14 +3,60 @@ use eframe::{App, Frame as EFrame}; use egui::{CentralPanel, Context}; use pwsp::{ types::socket::Request, - utils::{ - daemon::{get_daemon_config, is_daemon_running}, - gui::make_request_sync, - }, + utils::{daemon::get_daemon_config, gui::make_request_async}, }; +use std::time::{Duration, Instant}; impl App for SoundpadGui { fn update(&mut self, ctx: &Context, _frame: &mut EFrame) { + let mut seek_requests = vec![]; + let mut volume_requests = vec![]; + + for (id, ui_state) in &mut self.app_state.track_ui_states { + if ui_state.position_dragged { + seek_requests.push((*id, ui_state.position_slider_value)); + } + if ui_state.volume_dragged { + volume_requests.push((*id, ui_state.volume_slider_value)); + ui_state.volume_dragged = false; + } + } + + for (id, pos) in seek_requests { + make_request_async(Request::seek(pos, Some(id))); + if let Some(ui_state) = self.app_state.track_ui_states.get_mut(&id) { + ui_state.position_dragged = false; + ui_state.ignore_position_update_until = + Some(Instant::now() + Duration::from_millis(300)); + } + } + + for (id, vol) in volume_requests { + make_request_async(Request::set_volume(vol, Some(id))); + if let Some(ui_state) = self.app_state.track_ui_states.get_mut(&id) { + ui_state.volume_dragged = false; + ui_state.ignore_volume_update_until = + Some(Instant::now() + Duration::from_millis(300)); + } + } + + if self.app_state.volume_dragged { + make_request_async(Request::set_volume( + self.app_state.volume_slider_value, + None, + )); + + self.app_state.volume_dragged = false; + self.app_state.ignore_volume_update_until = + Some(Instant::now() + Duration::from_millis(300)); + + if self.config.save_volume { + let mut daemon_config = get_daemon_config(); + daemon_config.default_volume = Some(self.app_state.volume_slider_value); + daemon_config.save_to_file().ok(); + } + } + { let guard = self.audio_player_state_shared.lock().unwrap(); self.audio_player_state = guard.clone(); @@ -29,7 +75,7 @@ impl App for SoundpadGui { self.handle_input(ctx); CentralPanel::default().show(ctx, |ui| { - if !is_daemon_running().unwrap() { + if !self.audio_player_state.is_daemon_running { self.draw_waiting_for_daemon(ui); return; } @@ -49,36 +95,6 @@ impl App for SoundpadGui { } }); - if self.app_state.position_dragged { - make_request_sync(Request::seek(self.app_state.position_slider_value)).ok(); - let mut guard = self.audio_player_state_shared.lock().unwrap(); - guard.new_position = Some(self.app_state.position_slider_value); - guard.position = self.app_state.position_slider_value; - self.app_state.position_dragged = false; - } else { - self.app_state.position_slider_value = self.audio_player_state.position; - } - - if self.app_state.volume_dragged { - let new_volume = self.app_state.volume_slider_value; - - make_request_sync(Request::set_volume(new_volume)).ok(); - - let mut guard = self.audio_player_state_shared.lock().unwrap(); - guard.new_volume = Some(self.app_state.volume_slider_value); - guard.volume = self.app_state.volume_slider_value; - - self.app_state.volume_dragged = false; - - if self.config.save_volume { - let mut daemon_config = get_daemon_config(); - daemon_config.default_volume = Some(new_volume); - daemon_config.save_to_file().ok(); - } - } else { - self.app_state.volume_slider_value = self.audio_player_state.volume; - } - ctx.request_repaint_after_secs(1.0 / 60.0); } } diff --git a/src/types/audio_player.rs b/src/types/audio_player.rs index 0c35612..e59d352 100644 --- a/src/types/audio_player.rs +++ b/src/types/audio_player.rs @@ -8,6 +8,7 @@ use crate::{ use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source}; use serde::{Deserialize, Serialize}; use std::{ + collections::HashMap, error::Error, fs, path::{Path, PathBuf}, @@ -22,19 +23,35 @@ pub enum PlayerState { Playing, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TrackInfo { + pub id: u32, + pub path: PathBuf, + pub duration: Option, + pub position: f32, + pub volume: f32, + pub looped: bool, + pub paused: bool, +} + +pub struct PlayingSound { + pub id: u32, + pub sink: Sink, + pub path: PathBuf, + pub duration: Option, + pub looped: bool, + pub volume: f32, +} + pub struct AudioPlayer { - _stream_handle: OutputStream, - sink: Sink, + pub stream_handle: OutputStream, + pub tracks: HashMap, + pub next_id: u32, input_link_sender: Option>, pub current_input_device: Option, - pub volume: f32, - pub duration: Option, - - pub current_file_path: Option, - - pub looped: bool, + pub volume: f32, // Master volume } impl AudioPlayer { @@ -50,22 +67,16 @@ impl AudioPlayer { } let stream_handle = OutputStreamBuilder::open_default_stream()?; - let sink = Sink::connect_new(stream_handle.mixer()); - sink.set_volume(default_volume); let mut audio_player = AudioPlayer { - _stream_handle: stream_handle, - sink, + stream_handle, + tracks: HashMap::new(), + next_id: 1, input_link_sender: None, current_input_device: default_input_device.clone(), volume: default_volume, - duration: None, - - current_file_path: None, - - looped: false, }; if default_input_device.is_some() { @@ -132,74 +143,123 @@ impl AudioPlayer { Ok(()) } - pub fn pause(&mut self) { - if self.get_state() == PlayerState::Playing { - self.sink.pause(); - } - } - - pub fn resume(&mut self) { - if self.get_state() == PlayerState::Paused { - self.sink.play(); - } - } - - pub fn stop(&mut self) { - self.sink.stop(); - } - - pub fn is_paused(&self) -> bool { - self.sink.is_paused() - } - - pub fn get_state(&self) -> PlayerState { - if self.sink.len() == 0 { - return PlayerState::Stopped; - } - - if self.sink.is_paused() { - return PlayerState::Paused; - } - - PlayerState::Playing - } - - pub fn set_volume(&mut self, volume: f32) { - self.volume = volume; - self.sink.set_volume(volume); - } - - pub fn get_position(&self) -> f32 { - if self.get_state() == PlayerState::Stopped { - return 0.0; - } - - self.sink.get_pos().as_secs_f32() - } - - pub fn seek(&mut self, mut position: f32) -> Result<(), Box> { - if position < 0.0 { - position = 0.0; - } - - match self.sink.try_seek(Duration::from_secs_f32(position)) { - Ok(_) => Ok(()), - Err(err) => Err(err.into()), - } - } - - pub fn get_duration(&mut self) -> Result> { - if self.get_state() == PlayerState::Stopped { - Err("Nothing is playing right now".into()) + pub fn pause(&mut self, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.pause(); + } } else { - match self.duration { - Some(duration) => Ok(duration), - None => Err("Couldn't determine duration for current file".into()), + for sound in self.tracks.values_mut() { + sound.sink.pause(); } } } - pub async fn play(&mut self, file_path: &Path) -> Result<(), Box> { + pub fn resume(&mut self, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.play(); + } + } else { + for sound in self.tracks.values_mut() { + sound.sink.play(); + } + } + } + + pub fn stop(&mut self, id: Option) { + if let Some(id) = id { + self.tracks.remove(&id); + } else { + self.tracks.clear(); + } + } + + pub fn is_paused(&self) -> bool { + if self.tracks.is_empty() { + return false; + } + self.tracks.values().all(|s| s.sink.is_paused()) + } + + pub fn get_state(&self) -> PlayerState { + if self.tracks.is_empty() { + return PlayerState::Stopped; + } + + if self + .tracks + .values() + .any(|s| !s.sink.is_paused() && !s.sink.empty()) + { + return PlayerState::Playing; + } + + if self.is_paused() { + return PlayerState::Paused; + } + + PlayerState::Stopped + } + + pub fn set_volume(&mut self, volume: f32, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.volume = volume; + sound.sink.set_volume(self.volume * volume); + } + } else { + self.volume = volume; + for sound in self.tracks.values_mut() { + sound.sink.set_volume(self.volume * sound.volume); + } + } + } + + pub fn get_position(&self, id: Option) -> f32 { + if let Some(id) = id { + if let Some(sound) = self.tracks.get(&id) { + return sound.sink.get_pos().as_secs_f32(); + } + } else if let Some(sound) = self.tracks.values().last() { + // Fallback to last added track if no ID + return sound.sink.get_pos().as_secs_f32(); + } + 0.0 + } + + pub fn seek(&mut self, position: f32, id: Option) -> Result<(), Box> { + let position = if position < 0.0 { 0.0 } else { position }; + + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.sink.try_seek(Duration::from_secs_f32(position))?; + } + } else { + // Seek all? Or last? Let's seek all for now if no ID provided + for sound in self.tracks.values_mut() { + sound.sink.try_seek(Duration::from_secs_f32(position)).ok(); + } + } + Ok(()) + } + + pub fn get_duration(&mut self, id: Option) -> Result> { + if let Some(id) = id { + if let Some(sound) = self.tracks.get(&id) { + return sound.duration.ok_or("Unknown duration".into()); + } + } else if let Some(sound) = self.tracks.values().last() { + return sound.duration.ok_or("Unknown duration".into()); + } + Err("No track playing".into()) + } + + pub async fn play( + &mut self, + file_path: &Path, + concurrent: bool, + ) -> Result> { if !file_path.exists() { return Err(format!("File does not exist: {}", file_path.display()).into()); } @@ -207,30 +267,93 @@ impl AudioPlayer { let file = fs::File::open(file_path)?; match Decoder::try_from(file) { Ok(source) => { - self.current_file_path = Some(file_path.to_path_buf()); - - if let Some(duration) = source.total_duration() { - self.duration = Some(duration.as_secs_f32()); - } else { - self.duration = None; + if !concurrent { + self.tracks.clear(); } - self.sink.stop(); - self.sink.append(source); - self.sink.play(); + let id = self.next_id; + self.next_id += 1; + + let duration = source.total_duration().map(|d| d.as_secs_f32()); + + let sink = Sink::connect_new(self.stream_handle.mixer()); + sink.set_volume(self.volume); // Default volume is 1.0 * master + sink.append(source); + sink.play(); + + let sound = PlayingSound { + id, + sink, + path: file_path.to_path_buf(), + duration, + looped: false, + volume: 1.0, + }; + + self.tracks.insert(id, sound); + self.link_devices().await?; - Ok(()) + Ok(id) } Err(err) => Err(err.into()), } } - pub fn get_current_file_path(&mut self) -> &Option { - if self.get_state() == PlayerState::Stopped && !self.looped { - self.current_file_path = None; + pub fn set_loop(&mut self, enabled: bool, id: Option) { + if let Some(id) = id { + if let Some(sound) = self.tracks.get_mut(&id) { + sound.looped = enabled; + } + } else { + // Set loop for all? Or just last? + // Let's set for all. + for sound in self.tracks.values_mut() { + sound.looped = enabled; + } } - &self.current_file_path + } + + pub fn get_tracks(&self) -> Vec { + let mut tracks: Vec<_> = self + .tracks + .values() + .map(|sound| TrackInfo { + id: sound.id, + path: sound.path.clone(), + duration: sound.duration, + position: sound.sink.get_pos().as_secs_f32(), + volume: sound.volume, + looped: sound.looped, + paused: sound.sink.is_paused(), + }) + .collect(); + tracks.sort_by_key(|t| t.id); + tracks + } + + pub async fn update(&mut self) { + let mut restarts = vec![]; + + for (id, sound) in &self.tracks { + if sound.sink.empty() && sound.looped { + restarts.push(*id); + } + } + + for id in restarts { + if let Some(sound) = self.tracks.get_mut(&id) { + if let Ok(file) = fs::File::open(&sound.path) { + if let Ok(source) = Decoder::try_from(file) { + sound.sink.append(source); + sound.sink.play(); + } + } + } + } + + self.tracks + .retain(|_, sound| !sound.sink.empty() || sound.looped); } pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box> { diff --git a/src/types/commands.rs b/src/types/commands.rs index d6b2a67..d0a66d1 100644 --- a/src/types/commands.rs +++ b/src/types/commands.rs @@ -12,13 +12,21 @@ pub trait Executable { pub struct PingCommand {} -pub struct PauseCommand {} +pub struct PauseCommand { + pub id: Option, +} -pub struct ResumeCommand {} +pub struct ResumeCommand { + pub id: Option, +} -pub struct TogglePauseCommand {} +pub struct TogglePauseCommand { + pub id: Option, +} -pub struct StopCommand {} +pub struct StopCommand { + pub id: Option, +} pub struct IsPausedCommand {} @@ -28,21 +36,28 @@ pub struct GetVolumeCommand {} pub struct SetVolumeCommand { pub volume: Option, + pub id: Option, } -pub struct GetPositionCommand {} +pub struct GetPositionCommand { + pub id: Option, +} pub struct SeekCommand { pub position: Option, + pub id: Option, } -pub struct GetDurationCommand {} +pub struct GetDurationCommand { + pub id: Option, +} pub struct PlayCommand { pub file_path: Option, + pub concurrent: Option, } -pub struct GetCurrentFilePathCommand {} +pub struct GetTracksCommand {} pub struct GetCurrentInputCommand {} @@ -52,13 +67,14 @@ pub struct SetCurrentInputCommand { pub name: Option, } -pub struct GetLoopCommand {} - pub struct SetLoopCommand { pub enabled: Option, + pub id: Option, } -pub struct ToggleLoopCommand {} +pub struct ToggleLoopCommand { + pub id: Option, +} #[async_trait] impl Executable for PingCommand { @@ -71,7 +87,7 @@ impl Executable for PingCommand { impl Executable for PauseCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.pause(); + audio_player.pause(self.id); Response::new(true, "Audio was paused") } } @@ -80,7 +96,7 @@ impl Executable for PauseCommand { impl Executable for ResumeCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.resume(); + audio_player.resume(self.id); Response::new(true, "Audio was resumed") } } @@ -94,12 +110,31 @@ impl Executable for TogglePauseCommand { return Response::new(false, "Audio is not playing"); } - if audio_player.is_paused() { - audio_player.resume(); - Response::new(true, "Audio was resumed") + // This logic is a bit tricky with multiple tracks. + // If ID is provided, toggle that track. + // If not, toggle global pause state? + // For now, let's just use pause/resume based on global state if no ID. + + if let Some(id) = self.id { + if let Some(track) = audio_player.tracks.get(&id) { + if track.sink.is_paused() { + audio_player.resume(Some(id)); + Response::new(true, "Audio was resumed") + } else { + audio_player.pause(Some(id)); + Response::new(true, "Audio was paused") + } + } else { + Response::new(false, "Track not found") + } } else { - audio_player.pause(); - Response::new(true, "Audio was paused") + if audio_player.is_paused() { + audio_player.resume(None); + Response::new(true, "Audio was resumed") + } else { + audio_player.pause(None); + Response::new(true, "Audio was paused") + } } } } @@ -108,7 +143,7 @@ impl Executable for TogglePauseCommand { impl Executable for StopCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.stop(); + audio_player.stop(self.id); Response::new(true, "Audio was stopped") } } @@ -145,7 +180,7 @@ impl Executable for SetVolumeCommand { async fn execute(&self) -> Response { if let Some(volume) = self.volume { let mut audio_player = get_audio_player().await.lock().await; - audio_player.set_volume(volume); + audio_player.set_volume(volume, self.id); Response::new(true, format!("Audio volume was set to {}", volume)) } else { Response::new(false, "Invalid volume value") @@ -157,7 +192,7 @@ impl Executable for SetVolumeCommand { impl Executable for GetPositionCommand { async fn execute(&self) -> Response { let audio_player = get_audio_player().await.lock().await; - let position = audio_player.get_position(); + let position = audio_player.get_position(self.id); Response::new(true, position.to_string()) } } @@ -167,7 +202,7 @@ impl Executable for SeekCommand { async fn execute(&self) -> Response { if let Some(position) = self.position { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.seek(position) { + match audio_player.seek(position, self.id) { Ok(_) => Response::new(true, format!("Audio position was set to {}", position)), Err(err) => Response::new(false, err.to_string()), } @@ -181,7 +216,7 @@ impl Executable for SeekCommand { impl Executable for GetDurationCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.get_duration() { + match audio_player.get_duration(self.id) { Ok(duration) => Response::new(true, duration.to_string()), Err(err) => Response::new(false, err.to_string()), } @@ -193,8 +228,11 @@ impl Executable for PlayCommand { async fn execute(&self) -> Response { if let Some(file_path) = &self.file_path { let mut audio_player = get_audio_player().await.lock().await; - match audio_player.play(file_path).await { - Ok(_) => Response::new(true, format!("Now playing {}", file_path.display())), + match audio_player + .play(file_path, self.concurrent.unwrap_or(false)) + .await + { + Ok(id) => Response::new(true, id.to_string()), Err(err) => Response::new(false, err.to_string()), } } else { @@ -204,15 +242,11 @@ impl Executable for PlayCommand { } #[async_trait] -impl Executable for GetCurrentFilePathCommand { +impl Executable for GetTracksCommand { async fn execute(&self) -> Response { - let mut audio_player = get_audio_player().await.lock().await; - let current_file_path = audio_player.get_current_file_path(); - if let Some(current_file_path) = current_file_path { - Response::new(true, current_file_path.to_str().unwrap()) - } else { - Response::new(false, "No file is playing") - } + let audio_player = get_audio_player().await.lock().await; + let tracks = audio_player.get_tracks(); + Response::new(true, serde_json::to_string(&tracks).unwrap()) } } @@ -265,14 +299,6 @@ impl Executable for SetCurrentInputCommand { } } -#[async_trait] -impl Executable for GetLoopCommand { - async fn execute(&self) -> Response { - let audio_player = get_audio_player().await.lock().await; - Response::new(true, audio_player.looped.to_string()) - } -} - #[async_trait] impl Executable for SetLoopCommand { async fn execute(&self) -> Response { @@ -280,7 +306,7 @@ impl Executable for SetLoopCommand { match self.enabled { Some(enabled) => { - audio_player.looped = enabled; + audio_player.set_loop(enabled, self.id); Response::new(true, format!("Loop was set to {}", enabled)) } None => Response::new(false, "Invalid enabled value"), @@ -292,7 +318,19 @@ impl Executable for SetLoopCommand { impl Executable for ToggleLoopCommand { async fn execute(&self) -> Response { let mut audio_player = get_audio_player().await.lock().await; - audio_player.looped = !audio_player.looped; - Response::new(true, format!("Loop was set to {}", audio_player.looped)) + if let Some(id) = self.id { + if let Some(track) = audio_player.tracks.get_mut(&id) { + track.looped = !track.looped; + Response::new(true, format!("Loop was set to {}", track.looped)) + } else { + Response::new(false, "Track not found") + } + } else { + // Toggle all? + for track in audio_player.tracks.values_mut() { + track.looped = !track.looped; + } + Response::new(true, "Loop toggled for all tracks") + } } } diff --git a/src/types/gui.rs b/src/types/gui.rs index 0fbe641..728f502 100644 --- a/src/types/gui.rs +++ b/src/types/gui.rs @@ -1,23 +1,37 @@ -use crate::types::audio_player::PlayerState; +use crate::types::audio_player::{PlayerState, TrackInfo}; use egui::Id; use std::{ collections::{HashMap, HashSet}, path::PathBuf, + time::Instant, }; #[derive(Default, Debug)] -pub struct AppState { - pub search_query: String, - +pub struct TrackUiState { pub position_slider_value: f32, pub volume_slider_value: f32, pub position_dragged: bool, pub volume_dragged: bool, + pub ignore_position_update_until: Option, + pub ignore_volume_update_until: Option, +} + +#[derive(Default, Debug)] +pub struct AppState { + pub search_query: String, + + pub track_ui_states: HashMap, + pub show_settings: bool, + pub volume_dragged: bool, + + pub volume_slider_value: f32, + + pub ignore_volume_update_until: Option, pub current_dir: Option, pub dirs: HashSet, @@ -33,17 +47,13 @@ pub struct AppState { pub struct AudioPlayerState { pub state: PlayerState, pub new_state: Option, - pub current_file_path: PathBuf, - pub is_paused: bool, - pub looped: bool, + pub tracks: Vec, - pub volume: f32, - pub new_volume: Option, - pub position: f32, - pub new_position: Option, - pub duration: f32, + pub volume: f32, // Master volume pub current_input: String, pub all_inputs: HashMap, + + pub is_daemon_running: bool, } diff --git a/src/types/socket.rs b/src/types/socket.rs index ac5db2d..1f35810 100644 --- a/src/types/socket.rs +++ b/src/types/socket.rs @@ -24,24 +24,54 @@ impl Request { Request::new("ping", vec![]) } - pub fn pause() -> Self { - Request::new("pause", vec![]) + pub fn pause(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("pause", args) } - pub fn resume() -> Self { - Request::new("resume", vec![]) + pub fn resume(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("resume", args) } - pub fn toggle_pause() -> Self { - Request::new("toggle_pause", vec![]) + pub fn toggle_pause(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("toggle_pause", args) } - pub fn stop() -> Self { - Request::new("stop", vec![]) + pub fn stop(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("stop", args) } - pub fn play(file_path: &str) -> Self { - Request::new("play", vec![("file_path", file_path)]) + pub fn play(file_path: &str, concurrent: bool) -> Self { + Request::new( + "play", + vec![ + ("file_path", file_path), + ("concurrent", &concurrent.to_string()), + ], + ) } pub fn get_is_paused() -> Self { @@ -52,20 +82,32 @@ impl Request { Request::new("get_volume", vec![]) } - pub fn get_position() -> Self { - Request::new("get_position", vec![]) + pub fn get_position(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("get_position", args) } - pub fn get_duration() -> Self { - Request::new("get_duration", vec![]) + pub fn get_duration(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("get_duration", args) } pub fn get_state() -> Self { Request::new("get_state", vec![]) } - pub fn get_current_file_path() -> Self { - Request::new("get_current_file_path", vec![]) + pub fn get_tracks() -> Self { + Request::new("get_tracks", vec![]) } pub fn get_input() -> Self { @@ -76,28 +118,42 @@ impl Request { Request::new("get_inputs", vec![]) } - pub fn set_volume(volume: f32) -> Self { - Request::new("set_volume", vec![("volume", &volume.to_string())]) + pub fn set_volume(volume: f32, id: Option) -> Self { + let mut args = vec![("volume".to_string(), volume.to_string())]; + if let Some(id) = id { + args.push(("id".to_string(), id.to_string())); + } + Request::new("set_volume".to_string(), args) } - pub fn seek(position: f32) -> Self { - Request::new("seek", vec![("position", &position.to_string())]) + pub fn seek(position: f32, id: Option) -> Self { + let mut args = vec![("position".to_string(), position.to_string())]; + if let Some(id) = id { + args.push(("id".to_string(), id.to_string())); + } + Request::new("seek".to_string(), args) } pub fn set_input(name: &str) -> Self { Request::new("set_input", vec![("input_name", name)]) } - pub fn get_loop() -> Self { - Request::new("get_loop", vec![]) + pub fn set_loop(enabled: &str, id: Option) -> Self { + let mut args = vec![("enabled".to_string(), enabled.to_string())]; + if let Some(id) = id { + args.push(("id".to_string(), id.to_string())); + } + Request::new("set_loop".to_string(), args) } - pub fn set_loop(enabled: &str) -> Self { - Request::new("set_loop", vec![("enabled", enabled)]) - } - - pub fn toggle_loop() -> Self { - Request::new("toggle_loop", vec![]) + pub fn toggle_loop(id: Option) -> Self { + let mut args = vec![]; + let id_str; + if let Some(id) = id { + id_str = id.to_string(); + args.push(("id", id_str.as_str())); + } + Request::new("toggle_loop", args) } } diff --git a/src/utils/commands.rs b/src/utils/commands.rs index 46b2889..0e6e7c3 100644 --- a/src/utils/commands.rs +++ b/src/utils/commands.rs @@ -3,12 +3,14 @@ use crate::types::{commands::*, socket::Request}; use std::path::PathBuf; pub fn parse_command(request: &Request) -> Option> { + let id = request.args.get("id").and_then(|s| s.parse::().ok()); + match request.name.as_str() { "ping" => Some(Box::new(PingCommand {})), - "pause" => Some(Box::new(PauseCommand {})), - "resume" => Some(Box::new(ResumeCommand {})), - "toggle_pause" => Some(Box::new(TogglePauseCommand {})), - "stop" => Some(Box::new(StopCommand {})), + "pause" => Some(Box::new(PauseCommand { id })), + "resume" => Some(Box::new(ResumeCommand { id })), + "toggle_pause" => Some(Box::new(TogglePauseCommand { id })), + "stop" => Some(Box::new(StopCommand { id })), "is_paused" => Some(Box::new(IsPausedCommand {})), "get_state" => Some(Box::new(GetStateCommand {})), "get_volume" => Some(Box::new(GetVolumeCommand {})), @@ -19,9 +21,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SetVolumeCommand { volume })) + Some(Box::new(SetVolumeCommand { volume, id })) } - "get_position" => Some(Box::new(GetPositionCommand {})), + "get_position" => Some(Box::new(GetPositionCommand { id })), "seek" => { let position = request .args @@ -29,9 +31,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SeekCommand { position })) + Some(Box::new(SeekCommand { position, id })) } - "get_duration" => Some(Box::new(GetDurationCommand {})), + "get_duration" => Some(Box::new(GetDurationCommand { id })), "play" => { let file_path = request .args @@ -39,16 +41,24 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(PlayCommand { file_path })) + let concurrent = request + .args + .get("concurrent") + .unwrap_or(&String::new()) + .parse::() + .ok(); + Some(Box::new(PlayCommand { + file_path, + concurrent, + })) } - "get_current_file_path" => Some(Box::new(GetCurrentFilePathCommand {})), + "get_tracks" => Some(Box::new(GetTracksCommand {})), "get_input" => Some(Box::new(GetCurrentInputCommand {})), "get_inputs" => Some(Box::new(GetAllInputsCommand {})), "set_input" => { let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned(); Some(Box::new(SetCurrentInputCommand { name })) } - "get_loop" => Some(Box::new(GetLoopCommand {})), "set_loop" => { let enabled = request .args @@ -56,9 +66,9 @@ pub fn parse_command(request: &Request) -> Option> { .unwrap_or(&String::new()) .parse::() .ok(); - Some(Box::new(SetLoopCommand { enabled })) + Some(Box::new(SetLoopCommand { enabled, id })) } - "toggle_loop" => Some(Box::new(ToggleLoopCommand {})), + "toggle_loop" => Some(Box::new(ToggleLoopCommand { id })), _ => None, } } diff --git a/src/utils/gui.rs b/src/utils/gui.rs index 81544c7..78af6ef 100644 --- a/src/utils/gui.rs +++ b/src/utils/gui.rs @@ -1,16 +1,15 @@ use crate::{ types::{ - audio_player::PlayerState, + audio_player::{PlayerState, TrackInfo}, config::GuiConfig, gui::AudioPlayerState, socket::{Request, Response}, }, - utils::daemon::{make_request, wait_for_daemon}, + utils::daemon::{is_daemon_running, make_request}, }; use std::{ collections::HashMap, error::Error, - path::PathBuf, sync::{Arc, Mutex}, }; use tokio::time::{Duration, sleep}; @@ -31,6 +30,12 @@ pub fn make_request_sync(request: Request) -> Result> { }) } +pub fn make_request_async(request: Request) { + tokio::spawn(async move { + make_request(request).await.ok(); + }); +} + pub fn format_time_pair(position: f32, duration: f32) -> String { fn format_time(seconds: f32) -> String { let total_seconds = seconds.round() as u32; @@ -47,75 +52,54 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc serde_json::from_str::(&state_res.message).unwrap(), false => PlayerState::default(), }; - let file_path = match file_path_res.status { - true => PathBuf::from(file_path_res.message), - false => PathBuf::new(), - }; - let is_paused = match is_paused_res.status { - true => is_paused_res.message == "true", - false => false, + let tracks = match tracks_res.status { + true => { + serde_json::from_str::>(&tracks_res.message).unwrap_or_default() + } + false => vec![], }; + let volume = match volume_res.status { true => volume_res.message.parse::().unwrap(), false => 0.0, }; - let position = match position_res.status { - true => position_res.message.parse::().unwrap(), - false => 0.0, - }; - let duration = match duration_res.status { - true => duration_res.message.parse::().unwrap(), - false => 0.0, - }; + let current_input = match current_input_res.status { true => current_input_res .message @@ -144,10 +128,6 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc>(), false => HashMap::new(), }; - let looped = match looped_res.status { - true => looped_res.message.parse::().unwrap_or_default(), - false => false, - }; { let mut guard = audio_player_state_shared.lock().unwrap(); @@ -159,26 +139,11 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc state, }; - guard.current_file_path = file_path; - guard.is_paused = is_paused; - guard.volume = match guard.new_volume { - Some(new_volume) => { - guard.new_volume = None; - new_volume - } - None => volume, - }; - guard.position = match guard.new_position { - Some(new_position) => { - guard.new_position = None; - new_position - } - None => position, - }; - guard.duration = if duration > 0.0 { duration } else { 1.0 }; + guard.tracks = tracks.clone(); + guard.volume = volume; guard.current_input = current_input; guard.all_inputs = all_inputs; - guard.looped = looped; + guard.is_daemon_running = true; } sleep(sleep_duration).await;