Compare commits

..

81 Commits

Author SHA1 Message Date
arabianq 76a5f069ab update descriptions in github actions to use new tags format 2025-09-26 23:31:51 +03:00
arabianq ae84b345a4 change pwsp.spec to use new tags format 2025-09-26 23:30:09 +03:00
arabianq cdd58a04ed change package version to 1.0.1 2025-09-26 23:29:14 +03:00
arabianq 271955c777 cargo update 2025-09-26 23:28:21 +03:00
arabianq 5b475d1f07 bump serde version to 1.0.227 2025-09-26 23:27:56 +03:00
arabianq 5509b80f3e change code to work with pipewire 0.9.2 2025-09-26 23:25:30 +03:00
arabianq 2a31865822 bump pipewire crate to version 0.9.2 2025-09-26 23:20:11 +03:00
arabianq 258467d5bc change: now, instead of the full path to the file, only its name is displayed at the top 2025-09-26 23:09:28 +03:00
arabianq 51ab5eacbc fix: too large directory names break the interface 2025-09-26 23:06:27 +03:00
arabian 1957a5e2fd Delete scripts directory 2025-09-26 01:43:08 +03:00
arabian 5f852343da Update README.md 2025-09-26 01:42:14 +03:00
arabian aee48c8f8d Update release-deb.yml
fix .deb uploading
2025-09-26 01:22:35 +03:00
arabian c63b220d92 Update release-deb.yml
change name
2025-09-26 01:17:48 +03:00
arabian a4708f1812 Create release-deb.yml 2025-09-26 01:17:28 +03:00
arabian 3754121ab5 Update and rename build-release.yml to release-archive.yml 2025-09-26 01:10:35 +03:00
arabian a665939137 Update build-release.yml 2025-09-26 01:01:58 +03:00
arabian 974fdc9411 Update build-release.yml
try to fix zip creation
2025-09-26 00:54:56 +03:00
arabian 9a1107fb41 Update build-release.yml
change workflow_dispatch description
2025-09-26 00:50:13 +03:00
arabian ad2c15f9e3 Update build-release.yml
add build dependencies installation
2025-09-26 00:49:40 +03:00
arabian 6d66b57d1b Create build-release.yml 2025-09-26 00:46:28 +03:00
arabian 869b67738c Update README.md
add installation on Arch Linux from AUR
2025-09-26 00:35:27 +03:00
arabianq af3e19d794 update README.md 2025-09-25 19:50:09 +03:00
arabianq b42498d188 remove build_rpm.sh and cargo-generate-rpm mentions. Now copr handles rpm builds 2025-09-25 18:48:19 +03:00
arabianq 60975110da fix pwsp.spec 2025-09-25 18:40:06 +03:00
arabianq 05f243b322 add missing files to pwsp.spec 2025-09-25 17:54:10 +03:00
arabianq 0188cac476 specify BuildRequires and %install manually in pwsp.spec 2025-09-25 17:15:37 +03:00
arabianq 3e93ba14e1 update pwsp.spec file 2025-09-25 17:01:34 +03:00
arabianq 939dbea12b update pwsp.spec file 2025-09-25 16:57:41 +03:00
arabianq a4d3111c6d update pwsp.spec file 2025-09-25 16:55:26 +03:00
arabianq 9053056dfa fix Source url in pwsp.spec 2025-09-25 16:51:01 +03:00
arabianq e9e3f67735 add pwsp.spec 2025-09-25 16:47:26 +03:00
arabianq 9238e6563b remove generate_rpm_spec.sh 2025-09-25 16:46:33 +03:00
arabianq 939b01b587 use --path . in generate_rpm_spec 2025-09-25 16:44:18 +03:00
arabianq 489878c813 add rust-pwsp.spec 2025-09-25 16:39:19 +03:00
arabianq c1c1bfd487 add generate_rpm_spec.sh and rust-pwsp.spec 2025-09-25 16:38:25 +03:00
arabianq 2028a728e0 fix .rpm configuration 2025-09-24 23:49:36 +03:00
arabianq e627e71cf6 new README 2025-09-24 23:39:30 +03:00
arabianq dfe7a7e971 1.0.0 rewrite 2025-09-24 22:51:34 +03:00
arabianq 0535744b30 clear README 2025-09-14 01:05:24 +03:00
arabianq 3170e9f30a remove everything 2025-09-14 01:05:07 +03:00
arabianq 7ddd2c0dab change version to 0.1.8 2025-09-13 18:31:22 +03:00
arabianq 8590dcceae cargo update 2025-09-13 18:30:57 +03:00
arabianq fb6714aeee better imports 2025-09-13 18:30:19 +03:00
arabianq 34886e44a6 PlayerState::PLAYING -> PlayerState::Playing; PlayerState::PAUSED -> PlayerState::Paused 2025-09-13 18:28:30 +03:00
arabianq ede04dd3f8 cargo clippy 2025-09-13 18:27:22 +03:00
arabianq ad892dda29 bump egui and eframe to 0.32.3 2025-09-13 18:26:12 +03:00
arabianq 3a02e9991c change version to 0.1.7 2025-09-05 22:20:34 +03:00
arabianq 5d9d20417b update egui and eframe to 0.32.2 2025-09-05 22:19:26 +03:00
arabianq d1994d7226 change version to 0.1.6 2025-08-21 16:42:36 +03:00
arabianq b526d67d2f update deps 2025-08-21 16:42:22 +03:00
arabianq c641ec4f31 fix crash when seeking 2025-08-21 16:40:37 +03:00
arabianq 5bce45c97f Merge branch 'main' of github.com:arabianq/pipewire-soundpad 2025-07-21 12:20:07 +03:00
arabianq adfdf7db69 version -> 0.1.5 2025-07-21 12:19:50 +03:00
arabianq 7a7e0f741a update dependencies 2025-07-21 12:19:31 +03:00
arabianq d86cd6c5d3 rodio -> 0.21.1 2025-07-21 12:15:01 +03:00
arabian 61ba71d38e Update README.md 2025-07-15 20:30:56 +03:00
arabianq 7116ccf487 version -> 0.1.4 2025-07-12 00:51:57 +03:00
arabianq 654ef0d973 fix incorrect dependency for deb package 2025-07-12 00:51:31 +03:00
arabianq e9756b0681 update Cargo.toml 2025-07-12 00:27:50 +03:00
arabianq f72fe588e3 egui, eframe -> 0.32.0; egui_material_icons -> 0.4.0 2025-07-12 00:24:47 +03:00
arabianq 53533c35ef update build scripts 2025-07-12 00:22:52 +03:00
arabianq 382ddb0ff2 update README 2025-07-12 00:16:24 +03:00
arabianq 714f81ab34 update screenshot.png 2025-07-12 00:14:22 +03:00
arabianq 3c028972fe new build scripts and .desktop file 2025-07-08 05:01:44 +03:00
arabianq 7e3ff23156 version -> 0.1.31 2025-07-06 22:55:54 +03:00
arabianq 09cec44a5b maximum volume -> 1.0 from 5.0 2025-07-06 22:55:27 +03:00
arabianq ead17d26a9 version -> 0.1.3 2025-07-06 22:28:05 +03:00
arabianq dfa1cfbb15 now use single settings file instead of many; minor refactoring 2025-07-06 22:26:33 +03:00
arabianq a70c991711 new app::run function 2025-07-06 21:08:50 +03:00
arabianq 8c0704ce57 move creation of dirs to the separate function 2025-07-06 21:03:09 +03:00
arabianq 00196bfe7f split main.rs into main.rs and app.rs 2025-07-06 20:59:35 +03:00
arabianq 428fb4064d bump pwsp version to 0.1.2 2025-04-27 01:15:00 +03:00
arabianq e2b003be31 bump edition to 2024 2025-04-27 01:14:38 +03:00
arabianq 79176432de fixed setting player position to 0 2025-04-27 01:09:07 +03:00
arabianq 9e0a307106 bump egui, eframe and rfd versions 2025-04-27 01:01:24 +03:00
arabianq 428565594d v0.1.1 - minor fixes 2025-02-11 00:11:27 +03:00
arabianq 1d0e3036e9 Added unlink() function 2025-02-10 22:54:27 +03:00
arabianq b31f4c8c45 Update README.md 2025-02-10 17:40:10 +03:00
arabianq 84b6f8ce20 Added Cargo.lock 2025-02-10 02:58:14 +03:00
arabianq c797b204a2 Merge branch 'main' of github.com:arabianq/pipewire-soundpad 2025-02-09 23:41:48 +03:00
arabian faa29d8805 Create LICENSE 2025-02-09 13:43:23 +03:00
17 changed files with 739 additions and 900 deletions
Generated
+612 -578
View File
File diff suppressed because it is too large Load Diff
+8 -8
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.2.0" version = "1.0.1"
edition = "2024" edition = "2024"
authors = ["arabian"] authors = ["arabian"]
description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients." description = "PWSP lets you play audio files through your microphone. Has both CLI and GUI clients."
@@ -12,23 +12,23 @@ keywords = ["soundpad", "pipewire", "linux", "cli", "gui"]
[dependencies] [dependencies]
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.47.1", features = ["full"] }
futures = { version = "0.3.31", features = ["thread-pool"] } futures = { version = "0.3.31", features = ["thread-pool"] }
async-trait = "0.1.89" async-trait = "0.1.89"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.227", features = ["derive"] }
serde_json = "1.0.145" serde_json = "1.0.145"
clap = { version = "4.5.53", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] } clap = { version = "4.5.48", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
dirs = "6.0.0" dirs = "6.0.0"
rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] } rodio = { version = "0.21.1", default-features = false, features = ["symphonia-all", "playback"] }
pipewire = "0.9.2" pipewire = "0.9.2"
rfd = "0.16.0" rfd = "0.15.4"
egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] } egui = { version = "0.32.3", default-features = false, features = ["default_fonts", "rayon"] }
eframe = { version = "0.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] } eframe = { version = "0.32.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
egui_material_icons = "0.5.0" egui_material_icons = "0.4.0"
[[bin]] [[bin]]
name = "pwsp-daemon" name = "pwsp-daemon"
-2
View File
@@ -1,9 +1,7 @@
[Unit] [Unit]
Description=Pipewire Soundpad Daemon Description=Pipewire Soundpad Daemon
After=pipewire.service
[Service] [Service]
ExecStartPre=/usr/bin/sleep 10
ExecStart=/usr/bin/pwsp-daemon ExecStart=/usr/bin/pwsp-daemon
Restart=no Restart=no
RuntimeDirectory=pwsp RuntimeDirectory=pwsp
+2 -2
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.2.0 Version: 1.0.1
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
@@ -26,7 +26,7 @@ GUI clients.}
%description %{_description} %description %{_description}
%prep %prep
%autosetup -n pipewire-soundpad-%{version} -p1 %autosetup -n pipewire-soundpad-v%{version} -p1
%build %build
cargo build --release --locked cargo build --release --locked
+9 -12
View File
@@ -14,17 +14,17 @@ struct Cli {
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
enum Commands { enum Commands {
/// Perform an action (ping, pause, resume, toggle-pause, stop, play) /// Perform an action (ping, pause, resume, stop, play)
Action { Action {
#[clap(subcommand)] #[clap(subcommand)]
action: Actions, action: Actions,
}, },
/// Get information from the player (is paused, volume, position, duration, state, current-file-path, input, inputs) /// Get information from the player (is paused, volume, position, state)
Get { Get {
#[clap(subcommand)] #[clap(subcommand)]
parameter: GetCommands, parameter: GetCommands,
}, },
/// Set information in the player (volume, position, input) /// Set information in the player (volume, position)
Set { Set {
#[clap(subcommand)] #[clap(subcommand)]
parameter: SetCommands, parameter: SetCommands,
@@ -39,8 +39,6 @@ enum Actions {
Pause, Pause,
/// Resume audio playback /// Resume audio playback
Resume, Resume,
/// Toggle pause
TogglePause,
/// Stop audio playback and clear the queue /// Stop audio playback and clear the queue
Stop, Stop,
/// Play a file /// Play a file
@@ -53,11 +51,11 @@ enum GetCommands {
IsPaused, IsPaused,
/// Playback volume /// Playback volume
Volume, Volume,
/// Playback position (in seconds) /// Playback position
Position, Position,
/// Duration of the current file /// Duration of the current file
Duration, Duration,
/// Player state (Playing, Paused or Stopped) /// Player state
State, State,
/// Current playing file path /// Current playing file path
CurrentFilePath, CurrentFilePath,
@@ -71,10 +69,10 @@ enum GetCommands {
enum SetCommands { enum SetCommands {
/// Playback volume /// Playback volume
Volume { volume: f32 }, Volume { volume: f32 },
/// Playback position (in seconds) /// Playback position
Position { position: f32 }, Position { position: f32 },
/// Audio input id (see pwsp-cli get inputs) /// Input
Input { name: String }, Input { id: u32 },
} }
#[tokio::main] #[tokio::main]
@@ -88,7 +86,6 @@ async fn main() -> Result<(), Box<dyn Error>> {
Actions::Ping => Request::ping(), Actions::Ping => Request::ping(),
Actions::Pause => Request::pause(), Actions::Pause => Request::pause(),
Actions::Resume => Request::resume(), Actions::Resume => Request::resume(),
Actions::TogglePause => Request::toggle_pause(),
Actions::Stop => Request::stop(), Actions::Stop => Request::stop(),
Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()), Actions::Play { file_path } => Request::play(file_path.to_str().unwrap()),
}, },
@@ -105,7 +102,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
Commands::Set { parameter } => match parameter { Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume } => Request::set_volume(volume), SetCommands::Volume { volume } => Request::set_volume(volume),
SetCommands::Position { position } => Request::seek(position), SetCommands::Position { position } => Request::seek(position),
SetCommands::Input { name } => Request::set_input(&name), SetCommands::Input { id } => Request::set_input(id),
}, },
}; };
+30 -56
View File
@@ -1,7 +1,7 @@
use crate::gui::{SUPPORTED_EXTENSIONS, SoundpadGui}; use crate::gui::SoundpadGui;
use egui::{ use egui::{
Align, AtomExt, Button, Color32, ComboBox, FontFamily, Label, Layout, RichText, ScrollArea, AtomExt, Button, Color32, ComboBox, FontFamily, Label, RichText, ScrollArea, Slider, TextEdit,
Slider, TextEdit, Ui, Vec2, Ui, Vec2,
}; };
use egui_material_icons::icons; use egui_material_icons::icons;
use pwsp::types::audio_player::PlayerState; use pwsp::types::audio_player::PlayerState;
@@ -48,15 +48,10 @@ impl SoundpadGui {
&mut self.config.save_scale_factor, &mut self.config.save_scale_factor,
"Always remember UI scale factor", "Always remember UI scale factor",
); );
let pause_on_exit_response = ui.checkbox(
&mut self.config.pause_on_exit,
"Pause audio playback when the window is closed",
);
if save_volume_response.changed() if save_volume_response.changed()
|| save_input_response.changed() || save_input_response.changed()
|| save_scale_response.changed() || save_scale_response.changed()
|| pause_on_exit_response.changed()
{ {
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
@@ -120,7 +115,7 @@ impl SoundpadGui {
let position_slider_width = ui.available_width() let position_slider_width = ui.available_width()
- (30.0 * 3.0) - (30.0 * 3.0)
- default_slider_width - default_slider_width
- (ui.spacing().item_spacing.x * 6.0); - (ui.spacing().item_spacing.x * 5.0);
ui.spacing_mut().slider_width = position_slider_width; ui.spacing_mut().slider_width = position_slider_width;
let position_slider_response = ui.add_sized([30.0, 30.0], position_slider); let position_slider_response = ui.add_sized([30.0, 30.0], position_slider);
if position_slider_response.drag_stopped() { if position_slider_response.drag_stopped() {
@@ -187,8 +182,6 @@ impl SoundpadGui {
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
ScrollArea::vertical().id_salt(0).show(ui, |ui| { ScrollArea::vertical().id_salt(0).show(ui, |ui| {
ui.set_min_width(area_size.x);
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect(); let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
dirs.sort(); dirs.sort();
for path in dirs.iter() { for path in dirs.iter() {
@@ -198,43 +191,28 @@ impl SoundpadGui {
.map(|s| s.to_string_lossy().to_string()) .map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string()); .unwrap_or_else(|| path.to_string_lossy().to_string());
let mut dir_button_text = RichText::new(name.clone());
if let Some(current_dir) = &self.app_state.current_dir {
if current_dir.eq(path) {
dir_button_text = dir_button_text.color(Color32::WHITE);
}
}
let dir_button = let dir_button =
Button::new(dir_button_text.atom_max_width(area_size.x)).frame(false); Button::new(RichText::new(name).atom_max_width(area_size.x))
.frame(false);
let dir_button_response = ui.add(dir_button); let dir_button_response = ui.add(dir_button);
if dir_button_response.clicked() { if dir_button_response.clicked() {
self.open_dir(path); self.app_state.current_dir = Some(path.clone());
} }
let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false); let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false);
let delete_dir_button_response = let delete_dir_button_response =
ui.add_sized([18.0, 18.0], delete_dir_button); ui.add_sized([18.0, 18.0], delete_dir_button);
if delete_dir_button_response.clicked() { if delete_dir_button_response.clicked() {
self.remove_dir(&path.clone()); self.remove_dir(path.clone());
} }
}); });
} }
ui.horizontal(|ui| { ui.horizontal(|ui| {
let add_dirs_button = Button::new(icons::ICON_ADD).frame(false); let add_dir_button = egui::Button::new(icons::ICON_ADD).frame(false);
let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button); let add_dir_button_response = ui.add_sized([18.0, 18.0], add_dir_button);
if add_dirs_button_response.clicked() { if add_dir_button_response.clicked() {
self.add_dirs(); self.add_dir();
}
});
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
let play_file_button = Button::new("Play file");
let play_file_button_response = ui.add(play_file_button);
if play_file_button_response.clicked() {
self.open_file();
} }
}); });
}); });
@@ -242,14 +220,16 @@ impl SoundpadGui {
} }
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) { fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
let extensions = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "webm", "avi",
];
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let search_field = ui.add_sized( ui.add_sized(
[ui.available_width(), 22.0], [ui.available_width(), 22.0],
TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."), TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."),
); );
self.app_state.search_field_id = Some(search_field.id);
}); });
ui.separator(); ui.separator();
@@ -259,17 +239,18 @@ impl SoundpadGui {
ui.set_min_height(area_size.y); ui.set_min_height(area_size.y);
ui.vertical(|ui| { ui.vertical(|ui| {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect(); if let Some(path) = self.app_state.current_dir.clone() {
files.sort(); for entry in path.read_dir().unwrap() {
let entry = entry.unwrap();
let entry_path = entry.path();
for entry_path in files {
if entry_path.is_dir() { if entry_path.is_dir() {
continue; continue;
} }
if !SUPPORTED_EXTENSIONS if !extensions.contains(
.contains(&entry_path.extension().unwrap_or_default().to_str().unwrap()) &entry_path.extension().unwrap_or_default().to_str().unwrap(),
{ ) {
continue; continue;
} }
@@ -290,18 +271,11 @@ impl SoundpadGui {
continue; continue;
} }
let mut file_button_text = RichText::new(file_name); let file_button = Button::new(file_name).frame(false);
if let Some(current_file) = &self.app_state.selected_file {
if current_file.eq(&entry_path) {
file_button_text = file_button_text.color(Color32::WHITE);
}
}
let file_button = Button::new(file_button_text).frame(false);
let file_button_response = ui.add(file_button); let file_button_response = ui.add(file_button);
if file_button_response.clicked() { if file_button_response.clicked() {
self.play_file(&entry_path); self.play_file(entry_path);
self.app_state.selected_file = Some(entry_path); }
} }
} }
}); });
@@ -313,7 +287,7 @@ impl SoundpadGui {
ui.add_space(5.0); ui.add_space(5.0);
ui.horizontal_top(|ui| { ui.horizontal_top(|ui| {
// ---------- Microphone selection ---------- // ---------- Microphone selection ----------
let mut mics: Vec<(&String, &String)> = let mut mics: Vec<(&u32, &String)> =
self.audio_player_state.all_inputs.iter().collect(); self.audio_player_state.all_inputs.iter().collect();
mics.sort_by_key(|(k, _)| *k); mics.sort_by_key(|(k, _)| *k);
@@ -327,8 +301,8 @@ impl SoundpadGui {
.unwrap_or(&String::new()), .unwrap_or(&String::new()),
) )
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
for (name, nick) in mics { for (index, device) in mics {
ui.selectable_value(&mut selected_input, name.to_owned(), nick); ui.selectable_value(&mut selected_input, index.to_owned(), device);
} }
}); });
+5 -88
View File
@@ -1,106 +1,23 @@
use crate::gui::SoundpadGui; use crate::gui::SoundpadGui;
use egui::{Context, Key}; use egui::{Context, Key};
use std::path::PathBuf;
impl SoundpadGui { impl SoundpadGui {
pub fn handle_input(&mut self, ctx: &Context) { pub fn handle_input(&mut self, ctx: &Context) {
if ctx.memory(|reader| { reader.focused() }.is_some()) {
return;
}
ctx.input(|i| { ctx.input(|i| {
// Close app on espace
if i.key_pressed(Key::Escape) { if i.key_pressed(Key::Escape) {
std::process::exit(0); std::process::exit(0);
} }
// Open/close settings if !self.app_state.show_settings && i.key_pressed(Key::Space) {
if i.key_pressed(Key::I) {
self.app_state.show_settings = !self.app_state.show_settings;
}
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() {
self.play_file(&self.app_state.selected_file.clone().unwrap());
}
if !self.app_state.show_settings {
// Pause / resume audio on space
if i.key_pressed(Key::Space) {
self.play_toggle(); self.play_toggle();
} }
// Focus search field if i.key_pressed(Key::Slash) {
if i.key_pressed(Key::Slash) && self.app_state.search_field_id.is_some() { self.app_state.show_settings = !self.app_state.show_settings;
self.app_state.force_focus_id = self.app_state.search_field_id;
} }
// Iterate through dirs if there are some if self.app_state.show_settings && i.key_pressed(Key::Backspace) {
if i.modifiers.ctrl { self.app_state.show_settings = false;
let arrow_up_pressed = i.key_pressed(Key::ArrowUp);
let arrow_down_pressed = i.key_pressed(Key::ArrowDown);
if arrow_up_pressed || arrow_down_pressed {
if i.modifiers.shift && !self.app_state.dirs.is_empty() {
let mut dirs: Vec<PathBuf> =
self.app_state.dirs.iter().cloned().collect();
dirs.sort();
let current_dir_index: i8;
if let Some(current_dir) = &self.app_state.current_dir {
if let Some(index) = dirs.iter().position(|x| x == current_dir) {
current_dir_index = index as i8;
} else {
current_dir_index = -1;
}
} else {
current_dir_index = -1;
}
let mut new_dir_index: i8;
new_dir_index = current_dir_index - arrow_up_pressed as i8
+ arrow_down_pressed as i8;
if new_dir_index < 0 {
new_dir_index = (dirs.len() - 1) as i8;
} else if new_dir_index >= dirs.len() as i8 {
new_dir_index = 0;
}
self.open_dir(&dirs[new_dir_index as usize]);
} else if self.app_state.current_dir.is_some() {
let mut files: Vec<PathBuf> =
self.app_state.files.iter().cloned().collect();
files.sort();
let current_files_index: i64;
if let Some(selected_file) = &self.app_state.selected_file {
if let Some(index) = files.iter().position(|x| x == selected_file) {
current_files_index = index as i64;
} else {
current_files_index = -1;
}
} else {
current_files_index = -1;
}
let mut new_files_index: i64;
new_files_index = current_files_index - arrow_up_pressed as i64
+ arrow_down_pressed as i64;
if new_files_index < 0 {
new_files_index = (files.len() - 1) as i64;
} else if new_files_index >= files.len() as i64 {
new_files_index = 0;
}
self.app_state.selected_file =
Some(files[new_files_index as usize].clone());
}
}
}
} }
}); });
} }
+10 -40
View File
@@ -23,10 +23,6 @@ use std::{
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
const SUPPORTED_EXTENSIONS: [&str; 11] = [
"mp3", "wav", "ogg", "flac", "mp4", "m4a", "aac", "mov", "mkv", "webm", "avi",
];
struct SoundpadGui { struct SoundpadGui {
pub app_state: AppState, pub app_state: AppState,
pub config: GuiConfig, pub config: GuiConfig,
@@ -72,56 +68,36 @@ impl SoundpadGui {
}; };
} }
pub fn open_file(&mut self) { pub fn add_dir(&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);
}
}
pub fn add_dirs(&mut self) {
let file_dialog = FileDialog::new(); let file_dialog = FileDialog::new();
if let Some(paths) = file_dialog.pick_folders() { if let Some(path) = file_dialog.pick_folder() {
for path in paths {
self.app_state.dirs.insert(path); self.app_state.dirs.insert(path);
}
self.config.dirs = self.app_state.dirs.clone(); self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
} }
pub fn remove_dir(&mut self, path: &PathBuf) { pub fn remove_dir(&mut self, path: PathBuf) {
self.app_state.dirs.remove(path); self.app_state.dirs.remove(&path);
if let Some(current_dir) = &self.app_state.current_dir if let Some(current_dir) = &self.app_state.current_dir
&& current_dir == path && current_dir == &path
{ {
self.app_state.current_dir = None; self.app_state.current_dir = None;
self.app_state.files.clear();
} }
self.config.dirs = self.app_state.dirs.clone(); self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
pub fn open_dir(&mut self, path: &PathBuf) { pub fn play_file(&mut self, path: PathBuf) {
self.app_state.current_dir = Some(path.clone());
self.app_state.files = path
.read_dir()
.unwrap()
.filter_map(|res| res.ok())
.map(|entry| entry.path())
.collect();
}
pub fn play_file(&mut self, path: &PathBuf) {
make_request_sync(Request::play(path.to_str().unwrap())).ok(); make_request_sync(Request::play(path.to_str().unwrap())).ok();
} }
pub fn set_input(&mut self, name: String) { pub fn set_input(&mut self, id: u32) {
make_request_sync(Request::set_input(&name)).ok(); make_request_sync(Request::set_input(id)).ok();
if self.config.save_input { if self.config.save_input {
let mut daemon_config = get_daemon_config(); let mut daemon_config = get_daemon_config();
daemon_config.default_input_name = Some(name); daemon_config.default_input_id = Some(id);
daemon_config.save_to_file().ok(); daemon_config.save_to_file().ok();
} }
} }
@@ -152,13 +128,7 @@ pub async fn run() -> Result<(), Box<dyn Error>> {
Ok(Box::new(SoundpadGui::new(&cc.egui_ctx))) Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
}), }),
) { ) {
Ok(_) => { Ok(_) => Ok(()),
let config = get_gui_config();
if config.pause_on_exit {
make_request_sync(Request::pause()).ok();
}
Ok(())
}
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
} }
-7
View File
@@ -40,13 +40,6 @@ impl App for SoundpadGui {
} }
self.draw(ui).ok(); self.draw(ui).ok();
if let Some(force_focus_id) = self.app_state.force_focus_id {
ui.memory_mut(|reder| {
reder.request_focus(force_focus_id);
});
self.app_state.force_focus_id = None;
}
}); });
if self.app_state.position_dragged { if self.app_state.position_dragged {
+5 -9
View File
@@ -40,8 +40,8 @@ impl AudioPlayer {
let daemon_config = get_daemon_config(); let daemon_config = get_daemon_config();
let default_volume = daemon_config.default_volume.unwrap_or(1.0); let default_volume = daemon_config.default_volume.unwrap_or(1.0);
let mut default_input_device: Option<AudioDevice> = None; let mut default_input_device: Option<AudioDevice> = None;
if let Some(name) = daemon_config.default_input_name if let Some(id) = daemon_config.default_input_id
&& let Ok(device) = get_device(&name).await && let Ok(device) = get_device(id).await
&& device.device_type == DeviceType::Input && device.device_type == DeviceType::Input
{ {
default_input_device = Some(device); default_input_device = Some(device);
@@ -172,11 +172,7 @@ impl AudioPlayer {
self.sink.get_pos().as_secs_f32() self.sink.get_pos().as_secs_f32()
} }
pub fn seek(&mut self, mut position: f32) -> Result<(), Box<dyn Error>> { pub fn seek(&mut self, position: f32) -> Result<(), Box<dyn Error>> {
if position < 0.0 {
position = 0.0;
}
match self.sink.try_seek(Duration::from_secs_f32(position)) { match self.sink.try_seek(Duration::from_secs_f32(position)) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@@ -228,8 +224,8 @@ impl AudioPlayer {
&self.current_file_path &self.current_file_path
} }
pub async fn set_current_input_device(&mut self, name: &str) -> Result<(), Box<dyn Error>> { pub async fn set_current_input_device(&mut self, id: u32) -> Result<(), Box<dyn Error>> {
let input_device = get_device(name).await?; let input_device = get_device(id).await?;
if input_device.device_type != DeviceType::Input { if input_device.device_type != DeviceType::Input {
return Err("Selected device is not an input device".into()); return Err("Selected device is not an input device".into());
+6 -30
View File
@@ -1,5 +1,5 @@
use crate::{ use crate::{
types::{audio_player::PlayerState, socket::Response}, types::socket::Response,
utils::{daemon::get_audio_player, pipewire::get_all_devices}, utils::{daemon::get_audio_player, pipewire::get_all_devices},
}; };
use async_trait::async_trait; use async_trait::async_trait;
@@ -16,8 +16,6 @@ pub struct PauseCommand {}
pub struct ResumeCommand {} pub struct ResumeCommand {}
pub struct TogglePauseCommand {}
pub struct StopCommand {} pub struct StopCommand {}
pub struct IsPausedCommand {} pub struct IsPausedCommand {}
@@ -49,7 +47,7 @@ pub struct GetCurrentInputCommand {}
pub struct GetAllInputsCommand {} pub struct GetAllInputsCommand {}
pub struct SetCurrentInputCommand { pub struct SetCurrentInputCommand {
pub name: Option<String>, pub id: Option<u32>,
} }
#[async_trait] #[async_trait]
@@ -77,25 +75,6 @@ impl Executable for ResumeCommand {
} }
} }
#[async_trait]
impl Executable for TogglePauseCommand {
async fn execute(&self) -> Response {
let mut audio_player = get_audio_player().await.lock().await;
if audio_player.get_state() == PlayerState::Stopped {
return Response::new(false, "Audio is not playing");
}
if audio_player.is_paused() {
audio_player.resume();
Response::new(true, "Audio was resumed")
} else {
audio_player.pause();
Response::new(true, "Audio was paused")
}
}
}
#[async_trait] #[async_trait]
impl Executable for StopCommand { impl Executable for StopCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -213,10 +192,7 @@ impl Executable for GetCurrentInputCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
let audio_player = get_audio_player().await.lock().await; let audio_player = get_audio_player().await.lock().await;
if let Some(input_device) = &audio_player.current_input_device { if let Some(input_device) = &audio_player.current_input_device {
Response::new( Response::new(true, format!("{} - {}", input_device.id, input_device.nick))
true,
format!("{} - {}", input_device.name, input_device.nick),
)
} else { } else {
Response::new(false, "No input device selected") Response::new(false, "No input device selected")
} }
@@ -233,7 +209,7 @@ impl Executable for GetAllInputsCommand {
continue; continue;
} }
let string = format!("{} - {}", device.name, device.nick); let string = format!("{} - {}", device.id, device.nick);
input_devices_strings.push(string); input_devices_strings.push(string);
} }
let response_message = input_devices_strings.join("; "); let response_message = input_devices_strings.join("; ");
@@ -245,9 +221,9 @@ impl Executable for GetAllInputsCommand {
#[async_trait] #[async_trait]
impl Executable for SetCurrentInputCommand { impl Executable for SetCurrentInputCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
if let Some(name) = &self.name { if let Some(id) = self.id {
let mut audio_player = get_audio_player().await.lock().await; let mut audio_player = get_audio_player().await.lock().await;
match audio_player.set_current_input_device(name).await { match audio_player.set_current_input_device(id).await {
Ok(_) => Response::new(true, "Input device was set"), Ok(_) => Response::new(true, "Input device was set"),
Err(err) => Response::new(false, err.to_string()), Err(err) => Response::new(false, err.to_string()),
} }
+1 -3
View File
@@ -4,7 +4,7 @@ use std::{collections::HashSet, error::Error, fs, path::PathBuf};
#[derive(Default, Clone, Serialize, Deserialize)] #[derive(Default, Clone, Serialize, Deserialize)]
pub struct DaemonConfig { pub struct DaemonConfig {
pub default_input_name: Option<String>, pub default_input_id: Option<u32>,
pub default_volume: Option<f32>, pub default_volume: Option<f32>,
} }
@@ -36,7 +36,6 @@ pub struct GuiConfig {
pub save_volume: bool, pub save_volume: bool,
pub save_input: bool, pub save_input: bool,
pub save_scale_factor: bool, pub save_scale_factor: bool,
pub pause_on_exit: bool,
pub dirs: HashSet<PathBuf>, pub dirs: HashSet<PathBuf>,
} }
@@ -49,7 +48,6 @@ impl Default for GuiConfig {
save_volume: false, save_volume: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false,
dirs: HashSet::default(), dirs: HashSet::default(),
} }
+2 -11
View File
@@ -1,7 +1,4 @@
use crate::types::audio_player::PlayerState; use crate::types::audio_player::PlayerState;
use egui::Id;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::PathBuf, path::PathBuf,
@@ -21,12 +18,6 @@ pub struct AppState {
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: HashSet<PathBuf>, pub dirs: HashSet<PathBuf>,
pub selected_file: Option<PathBuf>,
pub files: HashSet<PathBuf>,
pub search_field_id: Option<Id>,
pub force_focus_id: Option<Id>,
} }
#[derive(Default, Debug, Clone)] #[derive(Default, Debug, Clone)]
@@ -43,6 +34,6 @@ pub struct AudioPlayerState {
pub new_position: Option<f32>, pub new_position: Option<f32>,
pub duration: f32, pub duration: f32,
pub current_input: String, pub current_input: u32,
pub all_inputs: HashMap<String, String>, pub all_inputs: HashMap<u32, String>,
} }
+2 -6
View File
@@ -32,10 +32,6 @@ impl Request {
Request::new("resume", vec![]) Request::new("resume", vec![])
} }
pub fn toggle_pause() -> Self {
Request::new("toggle_pause", vec![])
}
pub fn stop() -> Self { pub fn stop() -> Self {
Request::new("stop", vec![]) Request::new("stop", vec![])
} }
@@ -84,8 +80,8 @@ impl Request {
Request::new("seek", vec![("position", &position.to_string())]) Request::new("seek", vec![("position", &position.to_string())])
} }
pub fn set_input(name: &str) -> Self { pub fn set_input(id: u32) -> Self {
Request::new("set_input", vec![("input_name", name)]) Request::new("set_input", vec![("input_id", &id.to_string())])
} }
} }
+7 -3
View File
@@ -7,7 +7,6 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
"ping" => Some(Box::new(PingCommand {})), "ping" => Some(Box::new(PingCommand {})),
"pause" => Some(Box::new(PauseCommand {})), "pause" => Some(Box::new(PauseCommand {})),
"resume" => Some(Box::new(ResumeCommand {})), "resume" => Some(Box::new(ResumeCommand {})),
"toggle_pause" => Some(Box::new(TogglePauseCommand {})),
"stop" => Some(Box::new(StopCommand {})), "stop" => Some(Box::new(StopCommand {})),
"is_paused" => Some(Box::new(IsPausedCommand {})), "is_paused" => Some(Box::new(IsPausedCommand {})),
"get_state" => Some(Box::new(GetStateCommand {})), "get_state" => Some(Box::new(GetStateCommand {})),
@@ -45,8 +44,13 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
"get_input" => Some(Box::new(GetCurrentInputCommand {})), "get_input" => Some(Box::new(GetCurrentInputCommand {})),
"get_inputs" => Some(Box::new(GetAllInputsCommand {})), "get_inputs" => Some(Box::new(GetAllInputsCommand {})),
"set_input" => { "set_input" => {
let name = Some(request.args.get("input_name").unwrap_or(&String::new())).cloned(); let id = request
Some(Box::new(SetCurrentInputCommand { name })) .args
.get("input_id")
.unwrap_or(&String::new())
.parse::<u32>()
.ok();
Some(Box::new(SetCurrentInputCommand { id }))
} }
_ => None, _ => None,
} }
+11 -6
View File
@@ -96,8 +96,10 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
.collect::<Vec<&str>>() .collect::<Vec<&str>>()
.first() .first()
.unwrap() .unwrap()
.to_string(), .to_string()
false => String::new(), .parse::<u32>()
.unwrap_or_default(),
false => 0,
}; };
let all_inputs = match all_inputs_res.status { let all_inputs = match all_inputs_res.status {
true => all_inputs_res true => all_inputs_res
@@ -109,11 +111,14 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
if entry.is_empty() { if entry.is_empty() {
return None; return None;
} }
entry entry.split_once(" - ").and_then(|(k, v)| {
.split_once(" - ") k.trim()
.map(|(k, v)| (k.trim().to_string(), v.trim().to_string())) .parse::<u32>()
.ok()
.map(|key| (key, v.trim().to_string()))
}) })
.collect::<HashMap<String, String>>(), })
.collect(),
false => HashMap::new(), false => HashMap::new(),
}; };
+2 -12
View File
@@ -77,8 +77,6 @@ async fn pw_get_global_objects_thread(
main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>, main_sender: mpsc::Sender<(Option<AudioDevice>, Option<Port>)>,
pw_receiver: pipewire::channel::Receiver<Terminate>, pw_receiver: pipewire::channel::Receiver<Terminate>,
) { ) {
pipewire::init();
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop"); let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop");
// Stop main loop on Terminate message // Stop main loop on Terminate message
@@ -164,10 +162,6 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
input_device.input_fl = Some(port.clone()); input_device.input_fl = Some(port.clone());
input_device.input_fr = Some(port) input_device.input_fr = Some(port)
} }
"capture_MONO" => {
input_device.input_fl = Some(port.clone());
input_device.input_fr = Some(port);
}
_ => {} _ => {}
} }
} else if output_devices.contains_key(&node_id) { } else if output_devices.contains_key(&node_id) {
@@ -205,12 +199,12 @@ pub async fn get_all_devices() -> Result<(Vec<AudioDevice>, Vec<AudioDevice>), B
} }
} }
pub async fn get_device(device_name: &str) -> Result<AudioDevice, Box<dyn Error>> { pub async fn get_device(node_id: u32) -> Result<AudioDevice, Box<dyn Error>> {
let (mut input_devices, output_devices) = get_all_devices().await?; let (mut input_devices, output_devices) = get_all_devices().await?;
input_devices.extend(output_devices); input_devices.extend(output_devices);
for device in input_devices { for device in input_devices {
if device.name == device_name { if device.id == node_id {
return Ok(device); return Ok(device);
} }
} }
@@ -222,8 +216,6 @@ pub fn create_virtual_mic() -> Result<pipewire::channel::Sender<Terminate>, Box<
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
pipewire::init();
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop"); let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop");
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context"); let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context");
let core = context let core = context
@@ -265,8 +257,6 @@ pub fn create_link(
let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>(); let (pw_sender, pw_receiver) = pipewire::channel::channel::<Terminate>();
let _pw_thread = thread::spawn(move || { let _pw_thread = thread::spawn(move || {
pipewire::init();
let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop"); let main_loop = MainLoopRc::new(None).expect("Failed to initialize pipewire main loop");
let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context"); let context = ContextRc::new(&main_loop, None).expect("Failed to create pipewire context");
let core = context let core = context