1.0.0 rewrite

This commit is contained in:
2025-09-24 22:51:34 +03:00
parent 0535744b30
commit dfe7a7e971
32 changed files with 6973 additions and 7 deletions
+320
View File
@@ -0,0 +1,320 @@
use crate::gui::SoundpadGui;
use egui::{
Button, Color32, ComboBox, FontFamily, Label, RichText, ScrollArea, Slider, TextEdit, Ui, Vec2,
};
use egui_material_icons::icons;
use pwsp::types::audio_player::PlayerState;
use pwsp::utils::gui::format_time_pair;
use std::{error::Error, path::PathBuf};
impl SoundpadGui {
pub fn draw_waiting_for_daemon(&mut self, ui: &mut Ui) {
ui.centered_and_justified(|ui| {
ui.label(
RichText::new("Waiting for PWSP daemon to start...")
.size(34.0)
.monospace(),
);
});
}
pub fn draw_settings(&mut self, ui: &mut Ui) {
ui.vertical(|ui| {
ui.spacing_mut().item_spacing.y = 5.0;
// --------- Back Button and Title ----------
ui.horizontal_top(|ui| {
let back_button = Button::new(icons::ICON_ARROW_BACK).frame(false);
let back_button_response = ui.add(back_button);
if back_button_response.clicked() {
self.app_state.show_settings = false;
}
ui.add_space(ui.available_width() / 2.0 - 40.0);
ui.label(RichText::new("Settings").color(Color32::WHITE).monospace());
});
// --------------------------------
ui.separator();
ui.add_space(20.0);
// --------- Checkboxes ----------
let save_volume_response =
ui.checkbox(&mut self.config.save_volume, "Always remember volume");
let save_input_response =
ui.checkbox(&mut self.config.save_input, "Always remember microphone");
let save_scale_response = ui.checkbox(
&mut self.config.save_scale_factor,
"Always remember UI scale factor",
);
if save_volume_response.changed()
|| save_input_response.changed()
|| save_scale_response.changed()
{
self.config.save_to_file().ok();
}
// --------------------------------
});
}
pub fn draw(&mut self, ui: &mut Ui) -> Result<(), Box<dyn Error>> {
self.draw_header(ui);
self.draw_body(ui);
ui.separator();
self.draw_footer(ui);
Ok(())
}
fn draw_header(&mut self, ui: &mut Ui) {
ui.vertical_centered_justified(|ui| {
// Current file name
ui.label(
RichText::new(
self.audio_player_state
.current_file_path
.to_string_lossy()
.to_string(),
)
.color(Color32::WHITE)
.family(FontFamily::Monospace),
);
// Media controls
self.draw_controls(ui);
ui.separator();
});
}
fn draw_controls(&mut self, ui: &mut Ui) {
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,
})
.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();
}
// --------------------------------
// ---------- 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(1.0);
let default_slider_width = ui.spacing().slider_width;
let position_slider_width = ui.available_width()
- (30.0 * 3.0)
- default_slider_width
- (ui.spacing().item_spacing.x * 5.0);
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;
}
// --------------------------------
// ---------- Time Label ----------
let time_label = Label::new(
RichText::new(format_time_pair(
self.audio_player_state.position,
self.audio_player_state.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, 25.0], volume_icon);
// --------------------------------
// ---------- Volume Slider ----------
let volume_slider = Slider::new(&mut self.app_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().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;
}
// --------------------------------
});
}
fn draw_body(&mut self, ui: &mut Ui) {
let dirs_size = Vec2::new(ui.available_width() / 4.0, ui.available_height() - 40.0);
ui.horizontal(|ui| {
self.draw_dirs(ui, dirs_size);
ui.separator();
let files_size = Vec2::new(ui.available_width(), ui.available_height() - 40.0);
self.draw_files(ui, files_size);
});
}
fn draw_dirs(&mut self, ui: &mut Ui, area_size: Vec2) {
ui.vertical(|ui| {
ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y);
ScrollArea::vertical().id_salt(0).show(ui, |ui| {
let mut dirs: Vec<PathBuf> = self.app_state.dirs.iter().cloned().collect();
dirs.sort();
for path in dirs.iter() {
ui.horizontal(|ui| {
let name = path
.file_name()
.map(|s| s.to_string_lossy().to_string())
.unwrap_or_else(|| path.to_string_lossy().to_string());
let dir_button = Button::new(name).frame(false);
let dir_button_response = ui.add(dir_button);
if dir_button_response.clicked() {
self.app_state.current_dir = Some(path.clone());
}
let delete_dir_button = Button::new(icons::ICON_DELETE).frame(false);
let delete_dir_button_response =
ui.add_sized([18.0, 18.0], delete_dir_button);
if delete_dir_button_response.clicked() {
self.remove_dir(path.clone());
}
});
}
ui.horizontal(|ui| {
let add_dir_button = egui::Button::new(icons::ICON_ADD).frame(false);
let add_dir_button_response = ui.add_sized([18.0, 18.0], add_dir_button);
if add_dir_button_response.clicked() {
self.add_dir();
}
});
});
});
}
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.horizontal(|ui| {
ui.add_sized(
[ui.available_width(), 22.0],
TextEdit::singleline(&mut self.app_state.search_query).hint_text("Search..."),
);
});
ui.separator();
ScrollArea::vertical().id_salt(1).show(ui, |ui| {
ui.set_min_width(area_size.x);
ui.set_min_height(area_size.y);
ui.vertical(|ui| {
if let Some(path) = self.app_state.current_dir.clone() {
for entry in path.read_dir().unwrap() {
let entry = entry.unwrap();
let entry_path = entry.path();
if entry_path.is_dir() {
continue;
}
if !extensions.contains(
&entry_path.extension().unwrap_or_default().to_str().unwrap(),
) {
continue;
}
let file_name = entry_path
.file_name()
.unwrap()
.to_string_lossy()
.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 file_button = Button::new(file_name).frame(false);
let file_button_response = ui.add(file_button);
if file_button_response.clicked() {
self.play_file(entry_path);
}
}
}
});
});
});
}
fn draw_footer(&mut self, ui: &mut Ui) {
ui.add_space(5.0);
ui.horizontal_top(|ui| {
// ---------- Microphone selection ----------
let mut mics: Vec<(&u32, &String)> =
self.audio_player_state.all_inputs.iter().collect();
mics.sort_by_key(|(k, _)| *k);
let mut selected_input = self.audio_player_state.current_input.to_owned();
let prev_input = selected_input.to_owned();
ComboBox::from_label("Choose microphone")
.selected_text(
self.audio_player_state
.all_inputs
.get(&selected_input)
.unwrap_or(&String::new()),
)
.show_ui(ui, |ui| {
for (index, device) in mics {
ui.selectable_value(&mut selected_input, index.to_owned(), device);
}
});
if selected_input != prev_input {
self.set_input(selected_input);
}
// --------------------------------
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_response = ui.add_sized([18.0, 18.0], settings_button);
if settings_button_response.clicked() {
self.app_state.show_settings = true;
}
// --------------------------------
});
}
}
+24
View File
@@ -0,0 +1,24 @@
use crate::gui::SoundpadGui;
use egui::{Context, Key};
impl SoundpadGui {
pub fn handle_input(&mut self, ctx: &Context) {
ctx.input(|i| {
if i.key_pressed(Key::Escape) {
std::process::exit(0);
}
if !self.app_state.show_settings && i.key_pressed(Key::Space) {
self.play_toggle();
}
if i.key_pressed(Key::Slash) {
self.app_state.show_settings = !self.app_state.show_settings;
}
if self.app_state.show_settings && i.key_pressed(Key::Backspace) {
self.app_state.show_settings = false;
}
});
}
}
+134
View File
@@ -0,0 +1,134 @@
mod draw;
mod input;
mod update;
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
use egui::{Context, Vec2, ViewportBuilder};
use pwsp::{
types::{
audio_player::PlayerState,
config::GuiConfig,
gui::{AppState, AudioPlayerState},
socket::Request,
},
utils::{
daemon::get_daemon_config,
gui::{get_gui_config, make_request_sync, start_app_state_thread},
},
};
use rfd::FileDialog;
use std::path::PathBuf;
use std::{
error::Error,
sync::{Arc, Mutex},
};
struct SoundpadGui {
pub app_state: AppState,
pub config: GuiConfig,
pub audio_player_state: AudioPlayerState,
pub audio_player_state_shared: Arc<Mutex<AudioPlayerState>>,
}
impl SoundpadGui {
fn new(ctx: &Context) -> Self {
let audio_player_state = Arc::new(Mutex::new(AudioPlayerState::default()));
start_app_state_thread(audio_player_state.clone());
let config = get_gui_config();
ctx.set_zoom_factor(config.scale_factor);
let mut soundpad_gui = SoundpadGui {
app_state: AppState::default(),
config: config.clone(),
audio_player_state: AudioPlayerState::default(),
audio_player_state_shared: audio_player_state.clone(),
};
soundpad_gui.app_state.dirs = config.dirs;
soundpad_gui
}
pub fn play_toggle(&mut self) {
let mut guard = self.audio_player_state_shared.lock().unwrap();
guard.state = match guard.state {
PlayerState::Playing => {
make_request_sync(Request::pause()).ok();
guard.new_state = Some(PlayerState::Paused);
PlayerState::Paused
}
PlayerState::Paused => {
make_request_sync(Request::resume()).ok();
guard.new_state = Some(PlayerState::Playing);
PlayerState::Playing
}
PlayerState::Stopped => PlayerState::Stopped,
};
}
pub fn add_dir(&mut self) {
let file_dialog = FileDialog::new();
if let Some(path) = file_dialog.pick_folder() {
self.app_state.dirs.insert(path);
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
}
pub fn remove_dir(&mut self, path: PathBuf) {
self.app_state.dirs.remove(&path);
if let Some(current_dir) = &self.app_state.current_dir
&& current_dir == &path
{
self.app_state.current_dir = None;
}
self.config.dirs = self.app_state.dirs.clone();
self.config.save_to_file().ok();
}
pub fn play_file(&mut self, path: PathBuf) {
make_request_sync(Request::play(path.to_str().unwrap())).ok();
}
pub fn set_input(&mut self, id: u32) {
make_request_sync(Request::set_input(id)).ok();
if self.config.save_input {
let mut daemon_config = get_daemon_config();
daemon_config.default_input_id = Some(id);
daemon_config.save_to_file().ok();
}
}
}
pub async fn run() -> Result<(), Box<dyn Error>> {
const ICON: &[u8] = include_bytes!("../../assets/icon.png");
let options = NativeOptions {
vsync: true,
centered: true,
hardware_acceleration: HardwareAcceleration::Preferred,
viewport: ViewportBuilder::default()
.with_app_id("ru.arabianq.pwsp")
.with_inner_size(Vec2::new(1200.0, 800.0))
.with_min_inner_size(Vec2::new(800.0, 600.0))
.with_icon(from_png_bytes(ICON)?),
..Default::default()
};
match run_native(
"Pipewire Soundpad",
options,
Box::new(|cc| {
egui_material_icons::initialize(&cc.egui_ctx);
Ok(Box::new(SoundpadGui::new(&cc.egui_ctx)))
}),
) {
Ok(_) => Ok(()),
Err(e) => Err(e.into()),
}
}
+77
View File
@@ -0,0 +1,77 @@
use crate::gui::SoundpadGui;
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,
},
};
impl App for SoundpadGui {
fn update(&mut self, ctx: &Context, _frame: &mut EFrame) {
{
let guard = self.audio_player_state_shared.lock().unwrap();
self.audio_player_state = guard.clone();
}
let old_scale_factor = self.config.scale_factor;
let new_scale_factor = ctx.zoom_factor().clamp(0.5, 2.0);
ctx.set_zoom_factor(new_scale_factor);
self.config.scale_factor = new_scale_factor;
if new_scale_factor != old_scale_factor && self.config.save_scale_factor {
self.config.save_to_file().ok();
}
self.handle_input(ctx);
CentralPanel::default().show(ctx, |ui| {
if !is_daemon_running().unwrap() {
self.draw_waiting_for_daemon(ui);
return;
}
if self.app_state.show_settings {
self.draw_settings(ui);
return;
}
self.draw(ui).ok();
});
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);
}
}