Compare commits

..

20 Commits

Author SHA1 Message Date
arabianq 330c3d79d4 cargo update 2026-01-28 03:40:43 +03:00
arabianq dff20daace deps: bump clap to 4.5.55 2026-01-28 03:39:39 +03:00
arabianq cdc44328a8 docs: update README to include new features for collapsible audio tracks, drag and drop directories, and automatic device detection 2026-01-28 03:38:22 +03:00
arabianq ac61a71dcb feat: bump version to 1.5.0 2026-01-28 03:35:04 +03:00
arabianq 71c800c396 docs(assets): update screenshot image 2026-01-28 03:34:32 +03:00
arabianq 577a6d279b fix(daemon): remove unnecessary ExecStartPre sleep command 2026-01-28 03:32:29 +03:00
arabianq 49e01f0318 fix(gui): correct calculation of vertical separator's position 2026-01-28 03:31:54 +03:00
arabianq 5ea9b3b0ba feat(daemon): implementet get full-state command 2026-01-28 02:41:33 +03:00
arabianq ca85d4c369 refactor: remove redundant device linking in play method 2026-01-28 02:28:23 +03:00
arabianq 4499b1d3aa feat(gui): now directories can be reordered using drag and drop 2026-01-28 02:10:36 +03:00
arabianq d385e5356e refactor: simplify device retrieval in link_player_to_virtual_mic function 2026-01-28 01:30:03 +03:00
arabianq b4a0dc6a83 feat: now pwsp will automatically detect when input device is connected/disconnected and properly link/unlink it 2026-01-28 01:26:43 +03:00
arabianq 2e570b3bb0 fix: navigating through files using keyboard now works correctly with filtered files 2026-01-28 00:45:52 +03:00
arabianq ee4554286e refactor: improved filtering functionality 2026-01-28 00:45:20 +03:00
arabianq 2c6f0d932e refactor: refactor input handling for Enter key and directory navigation 2026-01-28 00:34:44 +03:00
arabianq 4e7606fdc6 feat: remove escape key functionality from input handling 2026-01-28 00:28:34 +03:00
arabianq 03df631690 refactor: enhance search field focus functionality and input handling 2026-01-28 00:28:08 +03:00
arabianq 6df826f210 feat: you can now collapse every audio track 2026-01-28 00:03:56 +03:00
arabianq cdf306cfe9 feat: make vertical separator in GUI adjustable 2026-01-27 23:51:14 +03:00
arabianq 74a436b171 fix: add serde default attribute to DaemonConfig and GuiConfig structs 2026-01-27 23:50:50 +03:00
19 changed files with 467 additions and 342 deletions
Generated
+74 -14
View File
@@ -182,7 +182,7 @@ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"cexpr", "cexpr",
"clang-sys", "clang-sys",
"itertools", "itertools 0.13.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
@@ -396,9 +396,9 @@ dependencies = [
[[package]] [[package]]
name = "clap" name = "clap"
version = "4.5.54" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394" checksum = "3e34525d5bbbd55da2bb745d34b36121baac88d07619a9a09cfcf4a6c0832785"
dependencies = [ dependencies = [
"clap_builder", "clap_builder",
"clap_derive", "clap_derive",
@@ -406,9 +406,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_builder" name = "clap_builder"
version = "4.5.54" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00" checksum = "59a20016a20a3da95bef50ec7238dbd09baeef4311dcdd38ec15aba69812fb61"
dependencies = [ dependencies = [
"anstyle", "anstyle",
"clap_lex", "clap_lex",
@@ -417,9 +417,9 @@ dependencies = [
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.5.49" version = "4.5.55"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671" checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5"
dependencies = [ dependencies = [
"heck", "heck",
"proc-macro2", "proc-macro2",
@@ -461,6 +461,16 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "concat-idents"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f76990911f2267d837d9d0ad060aa63aaad170af40904b29461734c339030d4d"
dependencies = [
"quote",
"syn",
]
[[package]] [[package]]
name = "concurrent-queue" name = "concurrent-queue"
version = "2.5.0" version = "2.5.0"
@@ -810,6 +820,29 @@ dependencies = [
"winit", "winit",
] ]
[[package]]
name = "egui_animation"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3db554dd3784f469d804f7dc25d1b14a2e00f1608d7af60218ccbced720a6e8"
dependencies = [
"egui",
"hello_egui_utils",
"simple-easing",
]
[[package]]
name = "egui_dnd"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f535b8df7ca89f781954feaa505899c8955b550074ecafcaa3c040f67aec3d46"
dependencies = [
"egui",
"egui_animation",
"simple-easing",
"web-time",
]
[[package]] [[package]]
name = "egui_glow" name = "egui_glow"
version = "0.33.3" version = "0.33.3"
@@ -1180,6 +1213,16 @@ version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hello_egui_utils"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d09c2c7f3aa61624b1bec320be9029f30e06769f92e39ac99a6d6e01024ae8"
dependencies = [
"concat-idents",
"egui",
]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@@ -1327,6 +1370,15 @@ dependencies = [
"either", "either",
] ]
[[package]]
name = "itertools"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
dependencies = [
"either",
]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.17" version = "1.0.17"
@@ -2240,14 +2292,16 @@ checksum = "3eb8486b569e12e2c32ad3e204dbaba5e4b5b216e9367044f25f1dba42341773"
[[package]] [[package]]
name = "pwsp" name = "pwsp"
version = "1.4.0" version = "1.5.0"
dependencies = [ dependencies = [
"async-trait", "async-trait",
"clap", "clap",
"dirs", "dirs",
"eframe", "eframe",
"egui", "egui",
"egui_dnd",
"egui_material_icons", "egui_material_icons",
"itertools 0.14.0",
"pipewire", "pipewire",
"rfd", "rfd",
"rodio", "rodio",
@@ -2569,6 +2623,12 @@ version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2"
[[package]]
name = "simple-easing"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832ddd7df0d98d6fd93b973c330b7c8e0742d5cb8f1afc7dea89dba4d2531aa1"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.11" version = "0.4.11"
@@ -3941,18 +4001,18 @@ dependencies = [
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd" checksum = "71ddd76bcebeed25db614f82bf31a9f4222d3fbba300e6fb6c00afa26cbd4d9d"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.33" version = "0.8.34"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1" checksum = "d8187381b52e32220d50b255276aa16a084ec0a9017a0ca2152a1f55c539758d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -4015,9 +4075,9 @@ dependencies = [
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.16" version = "1.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfcd145825aace48cff44a8844de64bf75feec3080e0aa5cdbde72961ae51a65" checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
+4 -2
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "pwsp" name = "pwsp"
version = "1.4.0" version = "1.5.0"
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."
@@ -18,8 +18,9 @@ async-trait = "0.1.89"
serde = { version = "1.0.228", features = ["derive"] } serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.149" serde_json = "1.0.149"
clap = { version = "4.5.54", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] } clap = { version = "4.5.55", default-features = false, features = ["std", "suggestions", "help", "usage", "error-context", "derive"] }
dirs = "6.0.0" dirs = "6.0.0"
itertools = "0.14.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"
@@ -28,6 +29,7 @@ rfd = { version = "0.17.2", default-features = false, features = ["xdg-portal"]}
egui = { version = "0.33.3", default-features = false, features = ["default_fonts", "rayon"] } egui = { version = "0.33.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.33.3", default-features = false, features = ["default_fonts", "glow", "x11", "wayland"] }
egui_material_icons = "0.5.0" egui_material_icons = "0.5.0"
egui_dnd = "0.14.0"
[[bin]] [[bin]]
name = "pwsp-daemon" name = "pwsp-daemon"
+3 -1
View File
@@ -24,6 +24,9 @@ chats on platforms like **Discord, Zoom, or Teamspeak**.
* **Position slider** to fast-forward or rewind the audio. * **Position slider** to fast-forward or rewind the audio.
* **Persistent Configuration**: The list of added directories and your selected audio output device are saved * **Persistent Configuration**: The list of added directories and your selected audio output device are saved
automatically, so you won't need to reconfigure them every time you launch the application. automatically, so you won't need to reconfigure them every time you launch the application.
* **Collapsible Audio Tracks**: You can collapse every audio track to save space.
* **Drag and Drop Directories**: Reorder your sound directories easily using drag and drop.
* **Automatic Device Detection**: PWSP automatically detects when an input device is connected or disconnected and handles linking/unlinking.
# **⚙️ How It Works** # **⚙️ How It Works**
@@ -179,7 +182,6 @@ pwsp-cli --help
| Key | Action | | Key | Action |
| :----------------------- | :--------------------------------------------------- | | :----------------------- | :--------------------------------------------------- |
| **Esc** | Close application |
| **Space** | Pause / Resume audio | | **Space** | Pause / Resume audio |
| **Backspace** | Stop all audio tracks | | **Backspace** | Stop all audio tracks |
| **Enter** | Play selected file (stops all other tracks) | | **Enter** | Play selected file (stops all other tracks) |
-1
View File
@@ -3,7 +3,6 @@ Description=Pipewire Soundpad Daemon
After=pipewire.service 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
Binary file not shown.

Before

Width:  |  Height:  |  Size: 70 KiB

After

Width:  |  Height:  |  Size: 84 KiB

+1 -1
View File
@@ -4,7 +4,7 @@
%global cargo_install_lib 0 %global cargo_install_lib 0
Name: pwsp Name: pwsp
Version: 1.4.0 Version: 1.5.0
Release: %autorelease Release: %autorelease
Summary: Lets you play audio files through your microphone Summary: Lets you play audio files through your microphone
+3
View File
@@ -92,6 +92,8 @@ enum GetCommands {
Input, Input,
/// All audio inputs /// All audio inputs
Inputs, Inputs,
/// Full player state
FullState,
} }
#[derive(Subcommand, Debug)] #[derive(Subcommand, Debug)]
@@ -146,6 +148,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
GetCommands::Tracks => Request::get_tracks(), GetCommands::Tracks => Request::get_tracks(),
GetCommands::Input => Request::get_input(), GetCommands::Input => Request::get_input(),
GetCommands::Inputs => Request::get_inputs(), GetCommands::Inputs => Request::get_inputs(),
GetCommands::FullState => Request::get_full_state(),
}, },
Commands::Set { parameter } => match parameter { Commands::Set { parameter } => match parameter {
SetCommands::Volume { volume, id } => Request::set_volume(volume, id), SetCommands::Volume { volume, id } => Request::set_volume(volume, id),
+58 -44
View File
@@ -1,12 +1,13 @@
use crate::gui::{SUPPORTED_EXTENSIONS, SoundpadGui}; use crate::gui::SoundpadGui;
use egui::{ use egui::{
Align, AtomExt, Button, Color32, ComboBox, FontFamily, Label, Layout, RichText, ScrollArea, Align, AtomExt, Button, CollapsingHeader, Color32, ComboBox, CursorIcon, FontFamily, Label,
Slider, TextEdit, Ui, Vec2, Layout, RichText, ScrollArea, Sense, Slider, TextEdit, Ui, Vec2,
}; };
use egui_dnd::dnd;
use egui_material_icons::icons; use egui_material_icons::icons;
use pwsp::types::audio_player::TrackInfo; use pwsp::types::audio_player::TrackInfo;
use pwsp::utils::gui::format_time_pair; use pwsp::utils::gui::format_time_pair;
use std::{error::Error, path::PathBuf, time::Instant}; use std::{error::Error, time::Instant};
use pwsp::types::gui::AppState; use pwsp::types::gui::AppState;
@@ -95,11 +96,6 @@ impl SoundpadGui {
fn draw_header(&mut self, ui: &mut Ui) { fn draw_header(&mut self, ui: &mut Ui) {
ui.vertical_centered_justified(|ui| { ui.vertical_centered_justified(|ui| {
self.draw_controls(ui);
});
}
fn draw_controls(&mut self, ui: &mut Ui) {
if self.audio_player_state.tracks.is_empty() { if self.audio_player_state.tracks.is_empty() {
ui.label("No tracks playing"); ui.label("No tracks playing");
return; return;
@@ -109,7 +105,7 @@ impl SoundpadGui {
let mut action = None; let mut action = None;
for track in tracks { for track in tracks {
ui.label( CollapsingHeader::new(
RichText::new( RichText::new(
track track
.path .path
@@ -120,10 +116,13 @@ impl SoundpadGui {
) )
.color(Color32::WHITE) .color(Color32::WHITE)
.family(FontFamily::Monospace), .family(FontFamily::Monospace),
); )
.default_open(true)
.show(ui, |ui| {
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) { if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, &track) {
action = Some(act); action = Some(act);
} }
});
ui.separator(); ui.separator();
} }
@@ -135,6 +134,7 @@ impl SoundpadGui {
TrackAction::Stop(id) => self.stop(Some(id)), TrackAction::Stop(id) => self.stop(Some(id)),
} }
} }
});
} }
fn draw_track_control( fn draw_track_control(
@@ -260,11 +260,37 @@ impl SoundpadGui {
} }
fn draw_body(&mut self, ui: &mut Ui) { fn draw_body(&mut self, ui: &mut Ui) {
let dirs_size = Vec2::new(ui.available_width() / 4.0, ui.available_height() - 40.0); let left_panel_width = self
.config
.left_panel_width
.max(100.0)
.min(ui.available_width() - 100.0);
let dirs_size = Vec2::new(left_panel_width, ui.available_height() - 40.0);
ui.horizontal(|ui| { ui.horizontal(|ui| {
self.draw_dirs(ui, dirs_size); self.draw_dirs(ui, dirs_size);
ui.separator();
let (rect, response) = ui.allocate_at_least(
Vec2::new(ui.spacing().item_spacing.x, ui.available_height()),
Sense::click_and_drag(),
);
if ui.is_rect_visible(rect) {
let stroke = ui.visuals().widgets.noninteractive.bg_stroke;
ui.painter().vline(rect.center().x, rect.y_range(), stroke);
}
let vertical_separator_response =
response.on_hover_and_drag_cursor(CursorIcon::ResizeHorizontal);
if vertical_separator_response.dragged() {
self.config.left_panel_width += vertical_separator_response.drag_delta().x;
self.config.left_panel_width = self.config.left_panel_width.clamp(100.0, 500.0);
}
if vertical_separator_response.drag_stopped() {
self.config.save_to_file().ok();
}
let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0); let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0);
self.draw_files(ui, files_size); self.draw_files(ui, files_size);
@@ -279,10 +305,14 @@ impl SoundpadGui {
ScrollArea::vertical().id_salt(0).show(ui, |ui| { ScrollArea::vertical().id_salt(0).show(ui, |ui| {
ui.set_min_width(area_size.x); ui.set_min_width(area_size.x);
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect(); let mut dirs = self.app_state.dirs.clone();
dirs.sort();
for path in dirs.iter() { dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
let path = item.clone();
ui.horizontal(|ui| { ui.horizontal(|ui| {
handle.ui(ui, |ui| {
ui.label(icons::ICON_DRAG_INDICATOR);
});
let name = path let name = path
.file_name() .file_name()
.map(|s| s.to_string_lossy().to_string()) .map(|s| s.to_string_lossy().to_string())
@@ -290,7 +320,7 @@ impl SoundpadGui {
let mut dir_button_text = RichText::new(name.clone()); let mut dir_button_text = RichText::new(name.clone());
if let Some(current_dir) = &self.app_state.current_dir { if let Some(current_dir) = &self.app_state.current_dir {
if current_dir.eq(path) { if current_dir.eq(&path) {
dir_button_text = dir_button_text.color(Color32::WHITE); dir_button_text = dir_button_text.color(Color32::WHITE);
} }
} }
@@ -300,7 +330,7 @@ impl SoundpadGui {
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.open_dir(&path);
} }
let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false); let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false);
@@ -310,7 +340,8 @@ impl SoundpadGui {
self.remove_dir(&path.clone()); self.remove_dir(&path.clone());
} }
}); });
} });
self.app_state.dirs = dirs;
ui.horizontal(|ui| { ui.horizontal(|ui| {
let add_dirs_button = Button::new(icons::ICON_ADD).frame(false); let add_dirs_button = Button::new(icons::ICON_ADD).frame(false);
@@ -334,12 +365,17 @@ impl SoundpadGui {
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) { fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
ui.vertical(|ui| { ui.vertical(|ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
let search_field = ui.add_sized( let search_field_response = 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); if self.app_state.force_focus_search {
search_field_response.request_focus();
self.app_state.force_focus_search = false;
}
self.app_state.search_field_id = Some(search_field_response.id);
}); });
ui.separator(); ui.separator();
@@ -349,37 +385,15 @@ 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(); let files = self.get_filtered_files();
files.sort();
for entry_path in files { for entry_path in files {
if entry_path.is_dir() {
continue;
}
if !SUPPORTED_EXTENSIONS
.contains(&entry_path.extension().unwrap_or_default().to_str().unwrap())
{
continue;
}
let file_name = entry_path let file_name = entry_path
.file_name() .file_name()
.unwrap() .unwrap()
.to_string_lossy() .to_string_lossy()
.to_string(); .to_string();
let search_query = self
.app_state
.search_query
.to_lowercase()
.trim()
.to_string();
if !file_name.to_lowercase().contains(search_query.as_str()) {
continue;
}
let mut file_button_text = RichText::new(file_name); let mut file_button_text = RichText::new(file_name);
if let Some(current_file) = &self.app_state.selected_file { if let Some(current_file) = &self.app_state.selected_file {
if current_file.eq(&entry_path) { if current_file.eq(&entry_path) {
+51 -58
View File
@@ -1,30 +1,47 @@
use crate::gui::SoundpadGui; use crate::gui::SoundpadGui;
use egui::{Context, Key}; use egui::{Context, Key, Modifiers};
use std::path::PathBuf; use std::path::PathBuf;
impl SoundpadGui { impl SoundpadGui {
pub fn handle_input(&mut self, ctx: &Context) { fn key_pressed(&self, ctx: &Context, key: Key) -> bool {
if ctx.memory(|reader| { reader.focused() }.is_some()) { ctx.input(|i| i.key_pressed(key))
return;
} }
ctx.input(|i| { fn modifiers(&self, ctx: &Context) -> Modifiers {
// Close app on espace ctx.input(|i| i.modifiers)
if i.key_pressed(Key::Escape) {
std::process::exit(0);
} }
pub fn handle_input(&mut self, ctx: &Context) {
let modifiers = self.modifiers(ctx);
// Open/close settings // Open/close settings
if i.key_pressed(Key::I) { if self.key_pressed(ctx, Key::I) {
self.app_state.show_settings = !self.app_state.show_settings; self.app_state.show_settings = !self.app_state.show_settings;
} }
if i.key_pressed(Key::Enter) && self.app_state.selected_file.is_some() { if !self.app_state.show_settings {
// Pause / resume audio on space
if self.key_pressed(ctx, Key::Space) {
self.play_toggle();
}
// Stop all audio tracks on backspace
if self.key_pressed(ctx, Key::Backspace) {
self.stop(None);
}
// Focus search field
if self.key_pressed(ctx, Key::Slash) {
self.app_state.force_focus_search = true;
}
// Play selected file on Enter
if self.key_pressed(ctx, Key::Enter) && self.app_state.selected_file.is_some() {
let path = &self.app_state.selected_file.clone().unwrap(); let path = &self.app_state.selected_file.clone().unwrap();
if i.modifiers.ctrl { if modifiers.ctrl {
self.play_file(path, true); self.play_file(path, true);
} else if i.modifiers.shift } else if modifiers.shift
&& let Some(last_track) = self.audio_player_state.tracks.last() && let Some(last_track) = self.audio_player_state.tracks.last()
{ {
self.stop(Some(last_track.id)); self.stop(Some(last_track.id));
@@ -34,31 +51,12 @@ impl SoundpadGui {
} }
} }
if !self.app_state.show_settings { // Iterate through dirs and files with Ctrl + Up/Down
// Pause / resume audio on space let arrow_up_pressed = self.key_pressed(ctx, Key::ArrowUp);
if i.key_pressed(Key::Space) { let arrow_down_pressed = self.key_pressed(ctx, Key::ArrowDown);
self.play_toggle(); if modifiers.ctrl && (arrow_up_pressed || arrow_down_pressed) {
} if modifiers.shift && !self.app_state.dirs.is_empty() {
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
// 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;
}
// Iterate through dirs if there are some
if i.modifiers.ctrl {
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(); dirs.sort();
let current_dir_index: i8; let current_dir_index: i8;
@@ -74,8 +72,8 @@ impl SoundpadGui {
let mut new_dir_index: i8; let mut new_dir_index: i8;
new_dir_index = current_dir_index - arrow_up_pressed as i8 new_dir_index =
+ arrow_down_pressed as i8; current_dir_index - arrow_up_pressed as i8 + arrow_down_pressed as i8;
if new_dir_index < 0 { if new_dir_index < 0 {
new_dir_index = (dirs.len() - 1) as i8; new_dir_index = (dirs.len() - 1) as i8;
@@ -85,25 +83,22 @@ impl SoundpadGui {
self.open_dir(&dirs[new_dir_index as usize]); self.open_dir(&dirs[new_dir_index as usize]);
} else if self.app_state.current_dir.is_some() { } else if self.app_state.current_dir.is_some() {
let mut files: Vec<PathBuf> = let files = self.get_filtered_files();
self.app_state.files.iter().cloned().collect();
files.sort();
let current_files_index: i64; if files.is_empty() {
if let Some(selected_file) = &self.app_state.selected_file { return;
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; let current_files_index = self
.app_state
.selected_file
.as_ref()
.and_then(|f| files.iter().position(|x| x == f))
.map(|i| i as i64)
.unwrap_or(-1);
new_files_index = current_files_index - arrow_up_pressed as i64 let mut new_files_index =
+ arrow_down_pressed as i64; current_files_index - arrow_up_pressed as i64 + arrow_down_pressed as i64;
if new_files_index < 0 { if new_files_index < 0 {
new_files_index = (files.len() - 1) as i64; new_files_index = (files.len() - 1) as i64;
@@ -111,12 +106,10 @@ impl SoundpadGui {
new_files_index = 0; new_files_index = 0;
} }
self.app_state.selected_file = self.app_state.selected_file = Some(files[new_files_index as usize].clone());
Some(files[new_files_index as usize].clone());
} }
} }
} }
} // });
});
} }
} }
+45 -2
View File
@@ -4,6 +4,7 @@ mod update;
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native}; use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, Vec2, ViewportBuilder}; use egui::{Context, Vec2, ViewportBuilder};
use itertools::Itertools;
use pwsp::{ use pwsp::{
types::{ types::{
audio_player::PlayerState, audio_player::PlayerState,
@@ -87,15 +88,16 @@ impl SoundpadGui {
let file_dialog = FileDialog::new(); let file_dialog = FileDialog::new();
if let Some(paths) = file_dialog.pick_folders() { if let Some(paths) = file_dialog.pick_folders() {
for path in paths { for path in paths {
self.app_state.dirs.insert(path); 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.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.retain(|x| x != 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
{ {
@@ -145,6 +147,47 @@ impl SoundpadGui {
pub fn stop(&mut self, id: Option<u32>) { pub fn stop(&mut self, id: Option<u32>) {
make_request_async(Request::stop(id)); make_request_async(Request::stop(id));
} }
pub fn get_filtered_files(&self) -> Vec<PathBuf> {
let mut files: Vec<PathBuf> = self.app_state.files.iter().cloned().collect();
files.sort();
let search_query = self.app_state.search_query.to_lowercase();
let search_query = search_query.trim();
files
.into_iter()
.filter(|entry_path| {
if entry_path.is_dir() {
return false;
}
if !SUPPORTED_EXTENSIONS.contains(
&entry_path
.extension()
.unwrap_or_default()
.to_str()
.unwrap_or_default(),
) {
return false;
}
if !search_query.is_empty() {
let file_name = entry_path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.to_string();
if !file_name.to_lowercase().contains(search_query) {
return false;
}
}
true
})
.collect()
}
} }
pub async fn run() -> Result<(), Box<dyn Error>> { pub async fn run() -> Result<(), Box<dyn Error>> {
+12 -7
View File
@@ -9,6 +9,13 @@ use std::time::{Duration, Instant};
impl App for SoundpadGui { impl App for SoundpadGui {
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) { fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
// Save directories if changed
if !self.config.dirs.eq(&self.app_state.dirs) {
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
// Seek and volume requests
let mut seek_requests = vec![]; let mut seek_requests = vec![];
let mut volume_requests = vec![]; let mut volume_requests = vec![];
@@ -57,11 +64,13 @@ impl App for SoundpadGui {
} }
} }
// Sync audio player state
{ {
let guard = self.audio_player_state_shared.lock().unwrap(); let guard = self.audio_player_state_shared.lock().unwrap();
self.audio_player_state = guard.clone(); self.audio_player_state = guard.clone();
} }
// Handle scale factor changes
let old_scale_factor = self.config.scale_factor; let old_scale_factor = self.config.scale_factor;
let new_scale_factor = ctx.zoom_factor().clamp(0.5, 2.0); let new_scale_factor = ctx.zoom_factor().clamp(0.5, 2.0);
@@ -72,8 +81,10 @@ impl App for SoundpadGui {
self.config.save_to_file().ok(); self.config.save_to_file().ok();
} }
// Handle input
self.handle_input(ctx); self.handle_input(ctx);
// Draw UI
CentralPanel::default().show(ctx, |ui| { CentralPanel::default().show(ctx, |ui| {
if !self.audio_player_state.is_daemon_running { if !self.audio_player_state.is_daemon_running {
self.draw_waiting_for_daemon(ui); self.draw_waiting_for_daemon(ui);
@@ -86,15 +97,9 @@ 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;
}
}); });
// Request repaint
ctx.request_repaint_after_secs(1.0 / 60.0); ctx.request_repaint_after_secs(1.0 / 60.0);
} }
} }
+57 -36
View File
@@ -1,8 +1,8 @@
use crate::{ use crate::{
types::pipewire::{AudioDevice, DeviceType, Terminate}, types::pipewire::{DeviceType, Terminate},
utils::{ utils::{
daemon::get_daemon_config, daemon::get_daemon_config,
pipewire::{create_link, get_all_devices, get_device}, pipewire::{create_link, get_device},
}, },
}; };
use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source}; use rodio::{Decoder, OutputStream, OutputStreamBuilder, Sink, Source};
@@ -34,6 +34,15 @@ pub struct TrackInfo {
pub paused: bool, pub paused: bool,
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)]
pub struct FullState {
pub state: PlayerState,
pub tracks: Vec<TrackInfo>,
pub volume: f32,
pub current_input: String,
pub all_inputs: HashMap<String, String>,
}
pub struct PlayingSound { pub struct PlayingSound {
pub id: u32, pub id: u32,
pub sink: Sink, pub sink: Sink,
@@ -49,7 +58,7 @@ pub struct AudioPlayer {
pub next_id: u32, pub next_id: u32,
input_link_sender: Option<pipewire::channel::Sender<Terminate>>, input_link_sender: Option<pipewire::channel::Sender<Terminate>>,
pub current_input_device: Option<AudioDevice>, pub input_device_name: Option<String>,
pub volume: f32, // Master volume pub volume: f32, // Master volume
} }
@@ -58,13 +67,6 @@ impl AudioPlayer {
pub async fn new() -> Result<Self, Box<dyn Error>> { pub async fn new() -> Result<Self, Box<dyn Error>> {
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;
if let Some(name) = daemon_config.default_input_name
&& let Ok(device) = get_device(&name).await
&& device.device_type == DeviceType::Input
{
default_input_device = Some(device);
}
let stream_handle = OutputStreamBuilder::open_default_stream()?; let stream_handle = OutputStreamBuilder::open_default_stream()?;
@@ -74,12 +76,12 @@ impl AudioPlayer {
next_id: 1, next_id: 1,
input_link_sender: None, input_link_sender: None,
current_input_device: default_input_device.clone(), input_device_name: daemon_config.default_input_name.clone(),
volume: default_volume, volume: default_volume,
}; };
if default_input_device.is_some() { if audio_player.input_device_name.is_some() {
audio_player.link_devices().await?; audio_player.link_devices().await?;
} }
@@ -89,7 +91,10 @@ impl AudioPlayer {
fn abort_link_thread(&mut self) { fn abort_link_thread(&mut self) {
if let Some(sender) = &self.input_link_sender { if let Some(sender) = &self.input_link_sender {
match sender.send(Terminate {}) { match sender.send(Terminate {}) {
Ok(_) => println!("Sent terminate signal to link thread"), Ok(_) => {
println!("Sent terminate signal to link thread");
self.input_link_sender = None;
}
Err(_) => eprintln!("Failed to send terminate signal to link thread"), Err(_) => eprintln!("Failed to send terminate signal to link thread"),
} }
} }
@@ -98,42 +103,43 @@ impl AudioPlayer {
async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> { async fn link_devices(&mut self) -> Result<(), Box<dyn Error>> {
self.abort_link_thread(); self.abort_link_thread();
if self.current_input_device.is_none() { let input_device;
if let Some(input_device_name) = &self.input_device_name {
if let Ok(device) = get_device(input_device_name).await {
input_device = device;
} else {
eprintln!(
"Could not find selected input device {}, skipping device linking",
input_device_name
);
return Ok(());
}
} else {
eprintln!("No input device selected, skipping device linking"); eprintln!("No input device selected, skipping device linking");
return Ok(()); return Ok(());
} }
let (input_devices, _) = get_all_devices().await?; let daemon_input;
if let Ok(device) = get_device("pwsp-virtual-mic").await {
let mut pwsp_daemon_input: Option<AudioDevice> = None; daemon_input = device;
for input_device in input_devices { } else {
if input_device.name == "pwsp-virtual-mic" { eprintln!("Could not find pwsp-virtual-mic device, skipping device linking");
pwsp_daemon_input = Some(input_device);
break;
}
}
if pwsp_daemon_input.is_none() {
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
return Ok(()); return Ok(());
} }
let pwsp_daemon_input = pwsp_daemon_input.unwrap(); let Some(output_fl) = input_device.output_fl.clone() else {
let current_input_device = self.current_input_device.clone().unwrap();
let Some(output_fl) = current_input_device.output_fl.clone() else {
eprintln!("Failed to get pwsp-daemon output_fl"); eprintln!("Failed to get pwsp-daemon output_fl");
return Ok(()); return Ok(());
}; };
let Some(output_fr) = current_input_device.output_fr.clone() else { let Some(output_fr) = input_device.output_fr.clone() else {
eprintln!("Failed to get pwsp-daemon output_fr"); eprintln!("Failed to get pwsp-daemon output_fr");
return Ok(()); return Ok(());
}; };
let Some(input_fl) = pwsp_daemon_input.input_fl.clone() else { let Some(input_fl) = daemon_input.input_fl.clone() else {
eprintln!("Failed to get pwsp-daemon input_fl"); eprintln!("Failed to get pwsp-daemon input_fl");
return Ok(()); return Ok(());
}; };
let Some(input_fr) = pwsp_daemon_input.input_fr.clone() else { let Some(input_fr) = daemon_input.input_fr.clone() else {
eprintln!("Failed to get pwsp-daemon input_fr"); eprintln!("Failed to get pwsp-daemon input_fr");
return Ok(()); return Ok(());
}; };
@@ -292,8 +298,6 @@ impl AudioPlayer {
self.tracks.insert(id, sound); self.tracks.insert(id, sound);
self.link_devices().await?;
Ok(id) Ok(id)
} }
Err(err) => Err(err.into()), Err(err) => Err(err.into()),
@@ -333,6 +337,23 @@ impl AudioPlayer {
} }
pub async fn update(&mut self) { pub async fn update(&mut self) {
if let Some(input_device_name) = &self.input_device_name {
// Unlink devices if selected input device was removed
if self.input_link_sender.is_some() && get_device(input_device_name).await.is_err() {
// Selected input device was removed
eprintln!(
"Selected input device {} was removed, unlinking devices",
input_device_name
);
self.abort_link_thread();
}
// Link devices if not linked
else if self.input_link_sender.is_none() {
self.link_devices().await.ok();
}
}
// Handle looped sounds
let mut restarts = vec![]; let mut restarts = vec![];
for (id, sound) in &self.tracks { for (id, sound) in &self.tracks {
@@ -363,7 +384,7 @@ impl AudioPlayer {
return Err("Selected device is not an input device".into()); return Err("Selected device is not an input device".into());
} }
self.current_input_device = Some(input_device); self.input_device_name = Some(name.to_string());
self.link_devices().await?; self.link_devices().await?;
+50 -4
View File
@@ -1,9 +1,15 @@
use crate::{ use crate::{
types::{audio_player::PlayerState, socket::Response}, types::{
utils::{daemon::get_audio_player, pipewire::get_all_devices}, audio_player::{FullState, PlayerState},
socket::Response,
},
utils::{
daemon::get_audio_player,
pipewire::{get_all_devices, get_device},
},
}; };
use async_trait::async_trait; use async_trait::async_trait;
use std::path::PathBuf; use std::{collections::HashMap, path::PathBuf};
#[async_trait] #[async_trait]
pub trait Executable { pub trait Executable {
@@ -76,6 +82,8 @@ pub struct ToggleLoopCommand {
pub id: Option<u32>, pub id: Option<u32>,
} }
pub struct GetFullStateCommand {}
#[async_trait] #[async_trait]
impl Executable for PingCommand { impl Executable for PingCommand {
async fn execute(&self) -> Response { async fn execute(&self) -> Response {
@@ -254,11 +262,15 @@ impl Executable for GetTracksCommand {
impl Executable for GetCurrentInputCommand { 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_name) = &audio_player.input_device_name {
if let Ok(input_device) = get_device(input_device_name).await {
Response::new( Response::new(
true, true,
format!("{} - {}", input_device.name, input_device.nick), format!("{} - {}", input_device.name, input_device.nick),
) )
} else {
Response::new(false, "Failed to get current input device")
}
} else { } else {
Response::new(false, "No input device selected") Response::new(false, "No input device selected")
} }
@@ -334,3 +346,37 @@ impl Executable for ToggleLoopCommand {
} }
} }
} }
#[async_trait]
impl Executable for GetFullStateCommand {
async fn execute(&self) -> Response {
let (input_devices, _output_devices) = get_all_devices().await.unwrap();
let mut all_inputs = HashMap::new();
let mut current_input_nick = String::new();
let audio_player = get_audio_player().await.lock().await;
for device in input_devices {
if device.name == "pwsp-virtual-mic" {
continue;
}
if let Some(current_input_name) = &audio_player.input_device_name {
if device.name == *current_input_name {
current_input_nick = format!("{} - {}", device.name, device.nick);
}
}
all_inputs.insert(device.name, device.nick);
}
let full_state = FullState {
state: audio_player.get_state(),
tracks: audio_player.get_tracks(),
volume: audio_player.volume,
current_input: current_input_nick,
all_inputs,
};
Response::new(true, serde_json::to_string(&full_state).unwrap())
}
}
+7 -3
View File
@@ -1,8 +1,9 @@
use crate::utils::config::get_config_path; use crate::utils::config::get_config_path;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::{collections::HashSet, error::Error, fs, path::PathBuf}; use std::{error::Error, fs, path::PathBuf};
#[derive(Default, Clone, Serialize, Deserialize)] #[derive(Default, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct DaemonConfig { pub struct DaemonConfig {
pub default_input_name: Option<String>, pub default_input_name: Option<String>,
pub default_volume: Option<f32>, pub default_volume: Option<f32>,
@@ -30,28 +31,31 @@ impl DaemonConfig {
} }
#[derive(Clone, Serialize, Deserialize)] #[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct GuiConfig { pub struct GuiConfig {
pub scale_factor: f32, pub scale_factor: f32,
pub left_panel_width: f32,
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 pause_on_exit: bool,
pub dirs: HashSet<PathBuf>, pub dirs: Vec<PathBuf>,
} }
impl Default for GuiConfig { impl Default for GuiConfig {
fn default() -> Self { fn default() -> Self {
GuiConfig { GuiConfig {
scale_factor: 1.0, scale_factor: 1.0,
left_panel_width: 280.0,
save_volume: false, save_volume: false,
save_input: false, save_input: false,
save_scale_factor: false, save_scale_factor: false,
pause_on_exit: false, pause_on_exit: false,
dirs: HashSet::default(), dirs: vec![],
} }
} }
} }
+4 -4
View File
@@ -28,19 +28,19 @@ pub struct AppState {
pub show_settings: bool, pub show_settings: bool,
pub volume_dragged: bool, pub volume_dragged: bool,
pub force_focus_search: bool,
pub volume_slider_value: f32, pub volume_slider_value: f32,
pub search_field_id: Option<Id>,
pub ignore_volume_update_until: Option<Instant>, pub ignore_volume_update_until: Option<Instant>,
pub current_dir: Option<PathBuf>, pub current_dir: Option<PathBuf>,
pub dirs: HashSet<PathBuf>, pub dirs: Vec<PathBuf>,
pub selected_file: Option<PathBuf>, pub selected_file: Option<PathBuf>,
pub files: HashSet<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)]
+4
View File
@@ -155,6 +155,10 @@ impl Request {
} }
Request::new("toggle_loop", args) Request::new("toggle_loop", args)
} }
pub fn get_full_state() -> Self {
Request::new("get_full_state", vec![])
}
} }
#[derive(Default, Debug, Clone, Serialize, Deserialize)] #[derive(Default, Debug, Clone, Serialize, Deserialize)]
+1
View File
@@ -69,6 +69,7 @@ pub fn parse_command(request: &Request) -> Option<Box<dyn Executable + Send>> {
Some(Box::new(SetLoopCommand { enabled, id })) Some(Box::new(SetLoopCommand { enabled, id }))
} }
"toggle_loop" => Some(Box::new(ToggleLoopCommand { id })), "toggle_loop" => Some(Box::new(ToggleLoopCommand { id })),
"get_full_state" => Some(Box::new(GetFullStateCommand {})),
_ => None, _ => None,
} }
} }
+11 -27
View File
@@ -2,10 +2,9 @@ use crate::{
types::{ types::{
audio_player::AudioPlayer, audio_player::AudioPlayer,
config::DaemonConfig, config::DaemonConfig,
pipewire::AudioDevice,
socket::{Request, Response}, socket::{Request, Response},
}, },
utils::pipewire::{create_link, get_all_devices}, utils::pipewire::{create_link, get_device},
}; };
use std::path::PathBuf; use std::path::PathBuf;
use std::{error::Error, fs}; use std::{error::Error, fs};
@@ -36,37 +35,22 @@ pub fn get_daemon_config() -> DaemonConfig {
} }
pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> { pub async fn link_player_to_virtual_mic() -> Result<(), Box<dyn Error>> {
let (input_devices, output_devices) = get_all_devices().await?; let pwsp_daemon_output;
if let Ok(device) = get_device("alsa_playback.pwsp-daemon").await {
let mut pwsp_daemon_output: Option<AudioDevice> = None; pwsp_daemon_output = device;
for output_device in output_devices { } else {
if output_device.name == "alsa_playback.pwsp-daemon" { eprintln!("Could not find alsa_playback.pwsp-daemon device, skipping device linking");
pwsp_daemon_output = Some(output_device);
break;
}
}
if pwsp_daemon_output.is_none() {
eprintln!("Could not find pwsp-daemon output device, skipping device linking");
return Ok(()); return Ok(());
} }
let mut pwsp_daemon_input: Option<AudioDevice> = None; let pwsp_daemon_input;
for input_device in input_devices { if let Ok(device) = get_device("pwsp-virtual-mic").await {
if input_device.name == "pwsp-virtual-mic" { pwsp_daemon_input = device;
pwsp_daemon_input = Some(input_device); } else {
break; eprintln!("Could not find pwsp-virtual-mic device, skipping device linking");
}
}
if pwsp_daemon_input.is_none() {
eprintln!("Could not find pwsp-daemon input device, skipping device linking");
return Ok(()); return Ok(());
} }
let pwsp_daemon_output = pwsp_daemon_output.unwrap();
let pwsp_daemon_input = pwsp_daemon_input.unwrap();
let output_fl = pwsp_daemon_output let output_fl = pwsp_daemon_output
.clone() .clone()
.output_fl .output_fl
+16 -72
View File
@@ -1,6 +1,6 @@
use crate::{ use crate::{
types::{ types::{
audio_player::{PlayerState, TrackInfo}, audio_player::FullState,
config::GuiConfig, config::GuiConfig,
gui::AudioPlayerState, gui::AudioPlayerState,
socket::{Request, Response}, socket::{Request, Response},
@@ -8,7 +8,6 @@ use crate::{
utils::daemon::{is_daemon_running, make_request}, utils::daemon::{is_daemon_running, make_request},
}; };
use std::{ use std::{
collections::HashMap,
error::Error, error::Error,
sync::{Arc, Mutex}, sync::{Arc, Mutex},
}; };
@@ -63,73 +62,13 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
continue; continue;
} }
let state_req = Request::get_state(); let full_state_req = Request::get_full_state();
let tracks_req = Request::get_tracks(); let full_state_res = make_request(full_state_req).await.unwrap_or_default();
let volume_req = Request::get_volume();
let current_input_req = Request::get_input();
let all_inputs_req = Request::get_inputs();
let (state_res, tracks_res, volume_res, current_input_res, all_inputs_res) = tokio::join!( if full_state_res.status {
make_request(state_req), let full_state: FullState =
make_request(tracks_req), serde_json::from_str(&full_state_res.message).unwrap_or_default();
make_request(volume_req),
make_request(current_input_req),
make_request(all_inputs_req),
);
let state_res = state_res.unwrap_or_default();
let tracks_res = tracks_res.unwrap_or_default();
let volume_res = volume_res.unwrap_or_default();
let current_input_res = current_input_res.unwrap_or_default();
let all_inputs_res = all_inputs_res.unwrap_or_default();
let state = match state_res.status {
true => serde_json::from_str::<PlayerState>(&state_res.message).unwrap(),
false => PlayerState::default(),
};
let tracks = match tracks_res.status {
true => {
serde_json::from_str::<Vec<TrackInfo>>(&tracks_res.message).unwrap_or_default()
}
false => vec![],
};
let volume = match volume_res.status {
true => volume_res.message.parse::<f32>().unwrap(),
false => 0.0,
};
let current_input = match current_input_res.status {
true => current_input_res
.message
.as_str()
.split(" - ")
.collect::<Vec<&str>>()
.first()
.unwrap()
.to_string(),
false => String::new(),
};
let all_inputs = match all_inputs_res.status {
true => all_inputs_res
.message
.as_str()
.split(';')
.filter_map(|entry| {
let entry = entry.trim();
if entry.is_empty() {
return None;
}
entry
.split_once(" - ")
.map(|(k, v)| (k.trim().to_string(), v.trim().to_string()))
})
.collect::<HashMap<String, String>>(),
false => HashMap::new(),
};
{
let mut guard = audio_player_state_shared.lock().unwrap(); let mut guard = audio_player_state_shared.lock().unwrap();
guard.state = match guard.new_state.clone() { guard.state = match guard.new_state.clone() {
@@ -137,12 +76,17 @@ pub fn start_app_state_thread(audio_player_state_shared: Arc<Mutex<AudioPlayerSt
guard.new_state = None; guard.new_state = None;
new_state new_state
} }
None => state, None => full_state.state,
}; };
guard.tracks = tracks.clone(); guard.tracks = full_state.tracks;
guard.volume = volume; guard.volume = full_state.volume;
guard.current_input = current_input; guard.current_input = full_state
guard.all_inputs = all_inputs; .current_input
.split(" - ")
.next()
.unwrap_or_default()
.to_string();
guard.all_inputs = full_state.all_inputs;
guard.is_daemon_running = true; guard.is_daemon_running = true;
} }