mirror of
https://github.com/arabianq/pipewire-soundpad.git
synced 2026-06-19 20:23:33 +00:00
Compare commits
3 Commits
78960cdc10
...
660ece9866
| Author | SHA1 | Date | |
|---|---|---|---|
| 660ece9866 | |||
| f2dcf2e0fe | |||
| fe655be59a |
Generated
+1
@@ -3177,6 +3177,7 @@ dependencies = [
|
|||||||
"rfd",
|
"rfd",
|
||||||
"rodio",
|
"rodio",
|
||||||
"rust-i18n",
|
"rust-i18n",
|
||||||
|
"rustix 1.1.4",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sys-locale",
|
"sys-locale",
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ rfd = { version = "0.17.2", default-features = false, features = [
|
|||||||
opener = { version = "0.8.4", features = ["reveal"] }
|
opener = { version = "0.8.4", features = ["reveal"] }
|
||||||
system-fonts = "0.1.0"
|
system-fonts = "0.1.0"
|
||||||
anyhow = "1.0.102"
|
anyhow = "1.0.102"
|
||||||
|
rustix = { version = "1.1.4", features = ["process"] }
|
||||||
|
|
||||||
rust-i18n = "4.0.0"
|
rust-i18n = "4.0.0"
|
||||||
sys-locale = "0.3.2"
|
sys-locale = "0.3.2"
|
||||||
|
|||||||
+5
-1
@@ -39,7 +39,11 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let runtime_dir = get_runtime_dir();
|
let runtime_dir = get_runtime_dir();
|
||||||
|
|
||||||
let lock_file = fs::File::create(runtime_dir.join("daemon.lock"))?;
|
let lock_file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(runtime_dir.join("daemon.lock"))?;
|
||||||
lock_file.lock()?;
|
lock_file.lock()?;
|
||||||
|
|
||||||
let socket_path = runtime_dir.join("daemon.sock");
|
let socket_path = runtime_dir.join("daemon.sock");
|
||||||
|
|||||||
-1126
File diff suppressed because it is too large
Load Diff
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
mod draw;
|
|
||||||
mod input;
|
mod input;
|
||||||
mod update;
|
mod update;
|
||||||
|
mod views;
|
||||||
|
|
||||||
use anyhow::{Result, anyhow};
|
use anyhow::{Result, anyhow};
|
||||||
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
|
use eframe::{HardwareAcceleration, NativeOptions, icon_data::from_png_bytes, run_native};
|
||||||
|
|||||||
@@ -0,0 +1,432 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{
|
||||||
|
Align, AtomExt, Button, CollapsingHeader, Color32, CursorIcon, Layout, RichText, ScrollArea,
|
||||||
|
Sense, TextEdit, Ui, Vec2,
|
||||||
|
};
|
||||||
|
use egui_dnd::dnd;
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::{gui::AppState, gui::AudioPlayerState};
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::{cmp::Ordering, path::Path, path::PathBuf};
|
||||||
|
|
||||||
|
pub(crate) enum FileAction {
|
||||||
|
Play(PathBuf, bool),
|
||||||
|
StopAndPlay(u32, PathBuf, bool),
|
||||||
|
AssignHotkey(PathBuf),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_body(&mut self, ui: &mut Ui) {
|
||||||
|
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| {
|
||||||
|
self.draw_dirs(ui, dirs_size);
|
||||||
|
|
||||||
|
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);
|
||||||
|
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| {
|
||||||
|
ui.set_min_width(area_size.x);
|
||||||
|
|
||||||
|
let mut dirs = std::mem::take(&mut self.app_state.dirs);
|
||||||
|
let mut dir_to_open = None;
|
||||||
|
|
||||||
|
dnd(ui, "dnd_directories").show_vec(&mut dirs, |ui, item, handle, _state| {
|
||||||
|
let path = item;
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
handle.ui(ui, |ui| {
|
||||||
|
ui.label(ICON_DRAG_INDICATOR.codepoint);
|
||||||
|
});
|
||||||
|
let name = path
|
||||||
|
.file_name()
|
||||||
|
.map(|s| s.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
|
||||||
|
&& current_dir.eq(&*path)
|
||||||
|
{
|
||||||
|
dir_button_text = dir_button_text.color(Color32::WHITE);
|
||||||
|
}
|
||||||
|
|
||||||
|
let dir_button =
|
||||||
|
Button::new(dir_button_text.atom_max_width(area_size.x)).frame(false);
|
||||||
|
|
||||||
|
let dir_button_response = ui.add(dir_button);
|
||||||
|
if dir_button_response.clicked() {
|
||||||
|
dir_to_open = Some(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
let delete_dir_button = Button::new(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.app_state.dirs_to_remove.insert(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
dir_button_response.context_menu(|ui| {
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_NEW.codepoint,
|
||||||
|
t!("gui.context.dirs.open")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
dir_to_open = Some(path.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_BROWSER.codepoint,
|
||||||
|
t!("gui.context.dirs.open_in_fm")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Err(e) = opener::open(&path)
|
||||||
|
{
|
||||||
|
eprintln!("Failed to open file manager: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_DELETE.codepoint,
|
||||||
|
t!("gui.context.dirs.remove")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
self.app_state.dirs_to_remove.insert(path.clone());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
self.app_state.dirs = dirs;
|
||||||
|
|
||||||
|
if let Some(path) = dir_to_open {
|
||||||
|
self.open_dir(&path);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let add_dirs_button = Button::new(ICON_ADD).frame(false);
|
||||||
|
let add_dirs_button_response = ui.add_sized([18.0, 18.0], add_dirs_button);
|
||||||
|
if add_dirs_button_response.clicked() {
|
||||||
|
self.add_dirs();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||||
|
let play_file_button = Button::new(t!("gui.play_file_button"));
|
||||||
|
let play_file_button_response = ui.add(play_file_button);
|
||||||
|
if play_file_button_response.clicked() {
|
||||||
|
self.open_file();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files_search_field(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let search_field_response = ui.add_sized(
|
||||||
|
[ui.available_width(), 22.0],
|
||||||
|
TextEdit::singleline(&mut self.app_state.search_query)
|
||||||
|
.hint_text(t!("gui.search_placeholder")),
|
||||||
|
);
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files_list(&mut self, ui: &mut Ui, area_size: Vec2) {
|
||||||
|
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| {
|
||||||
|
let mut actions = Vec::new();
|
||||||
|
let files = self.get_filtered_files();
|
||||||
|
for entry_path in files {
|
||||||
|
Self::draw_tree_node(
|
||||||
|
ui,
|
||||||
|
entry_path,
|
||||||
|
&mut self.app_state,
|
||||||
|
&self.audio_player_state,
|
||||||
|
&mut actions,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
for action in actions {
|
||||||
|
match action {
|
||||||
|
FileAction::Play(path, concurrent) => self.play_file(&path, concurrent),
|
||||||
|
FileAction::StopAndPlay(id, path, concurrent) => {
|
||||||
|
self.stop(Some(id));
|
||||||
|
self.play_file(&path, concurrent);
|
||||||
|
}
|
||||||
|
FileAction::AssignHotkey(path) => {
|
||||||
|
self.app_state.assigning_hotkey_for_file = Some(path);
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_files(&mut self, ui: &mut Ui, area_size: Vec2) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
self.draw_files_search_field(ui);
|
||||||
|
ui.separator();
|
||||||
|
self.draw_files_list(ui, area_size);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node_dir(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
let dir_name = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
CollapsingHeader::new(dir_name)
|
||||||
|
.id_salt(&path)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
let children = if let Some(cached) = app_state.dir_cache.get(&path) {
|
||||||
|
cached.clone()
|
||||||
|
} else {
|
||||||
|
let mut read = Vec::new();
|
||||||
|
if let Ok(entries) = std::fs::read_dir(&path) {
|
||||||
|
for entry in entries.filter_map(|e| e.ok()) {
|
||||||
|
read.push(entry.path());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
read.sort_by(|a, b| {
|
||||||
|
let a_is_dir = a.is_dir();
|
||||||
|
let b_is_dir = b.is_dir();
|
||||||
|
if a_is_dir && !b_is_dir {
|
||||||
|
Ordering::Less
|
||||||
|
} else if !a_is_dir && b_is_dir {
|
||||||
|
Ordering::Greater
|
||||||
|
} else {
|
||||||
|
a.cmp(b)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app_state.dir_cache.insert(path.clone(), read.clone());
|
||||||
|
read
|
||||||
|
};
|
||||||
|
|
||||||
|
let search_query = app_state.search_query.to_lowercase();
|
||||||
|
let search_query = search_query.trim();
|
||||||
|
|
||||||
|
for child in children {
|
||||||
|
if !child.is_dir() {
|
||||||
|
if !crate::gui::SUPPORTED_EXTENSIONS.contains(
|
||||||
|
&child
|
||||||
|
.extension()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if !search_query.is_empty() {
|
||||||
|
let file_name = child
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
if !file_name.to_lowercase().contains(search_query) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Self::draw_tree_node(ui, child, app_state, audio_player_state, actions);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node_file(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
let file_name = path
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string();
|
||||||
|
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// Hotkey badge
|
||||||
|
let mut hotkey_badge = None;
|
||||||
|
for slot in &app_state.hotkey_config.slots {
|
||||||
|
if slot.action.name == "play"
|
||||||
|
&& let Some(file_path_str) = slot.action.args.get("file_path")
|
||||||
|
&& Path::new(file_path_str) == path
|
||||||
|
{
|
||||||
|
if let Some(chord) = &slot.key_chord {
|
||||||
|
hotkey_badge = Some(format!("[{}]", chord));
|
||||||
|
} else {
|
||||||
|
hotkey_badge = Some(format!("[{}]", slot.slot));
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(badge) = &hotkey_badge {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(badge)
|
||||||
|
.small()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::from_rgb(100, 200, 100)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let file_button_text = RichText::new(&file_name);
|
||||||
|
|
||||||
|
let file_button = Button::new(file_button_text).frame(false).truncate();
|
||||||
|
let file_button_response = ui.add(file_button);
|
||||||
|
if file_button_response.clicked() {
|
||||||
|
ui.input(|i| {
|
||||||
|
if i.modifiers.ctrl {
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
} else if i.modifiers.shift
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
|
||||||
|
} else {
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Context menu
|
||||||
|
file_button_response.context_menu(|ui| {
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_BOLT.codepoint,
|
||||||
|
t!("gui.context.files.play_solo")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), false));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_ADD.codepoint,
|
||||||
|
t!("gui.context.files.add_new")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::Play(path.clone(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_SWAP_HORIZ.codepoint,
|
||||||
|
t!("gui.context.files.replace_last")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Some(last_track) = audio_player_state.tracks.last()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::StopAndPlay(last_track.id, path.clone(), true));
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_OPEN_IN_BROWSER.codepoint,
|
||||||
|
t!("gui.context.files.show_in_fm")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
&& let Err(e) = opener::reveal(&path)
|
||||||
|
{
|
||||||
|
eprintln!("Failed to open file manager: {}", e);
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
if ui
|
||||||
|
.button(format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_KEYBOARD.codepoint,
|
||||||
|
t!("gui.context.files.asign_hotkey")
|
||||||
|
))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
actions.push(FileAction::AssignHotkey(path.clone()));
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_tree_node(
|
||||||
|
ui: &mut Ui,
|
||||||
|
path: std::path::PathBuf,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
audio_player_state: &AudioPlayerState,
|
||||||
|
actions: &mut Vec<FileAction>,
|
||||||
|
) {
|
||||||
|
if path.is_dir() {
|
||||||
|
Self::draw_tree_node_dir(ui, path, app_state, audio_player_state, actions);
|
||||||
|
} else {
|
||||||
|
Self::draw_tree_node_file(ui, path, app_state, audio_player_state, actions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{AtomExt, Button, ComboBox, Label, RichText, Slider, Ui, Vec2};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_footer(&mut self, ui: &mut Ui) {
|
||||||
|
ui.add_space(5.0);
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
// ---------- Microphone selection ----------
|
||||||
|
let mics = &self.audio_player_state.all_inputs_sorted;
|
||||||
|
|
||||||
|
let mut selected_input = self.audio_player_state.current_input.to_owned();
|
||||||
|
let prev_input = selected_input.to_owned();
|
||||||
|
ComboBox::from_label(t!("gui.choose_mic_select"))
|
||||||
|
.height(30.0)
|
||||||
|
.selected_text(
|
||||||
|
self.audio_player_state
|
||||||
|
.all_inputs
|
||||||
|
.get(&selected_input)
|
||||||
|
.unwrap_or(&String::new()),
|
||||||
|
)
|
||||||
|
.show_ui(ui, |ui| {
|
||||||
|
for (name, nick) in mics {
|
||||||
|
ui.selectable_value(&mut selected_input, name.clone(), nick);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if selected_input != prev_input {
|
||||||
|
self.set_input(selected_input);
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
// ---------- Master Volume Slider ----------
|
||||||
|
let volume_icon = Self::get_volume_icon(self.audio_player_state.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([18.0, 18.0], volume_label)
|
||||||
|
.on_hover_text(format!(
|
||||||
|
"Master Volume: {:.0}%",
|
||||||
|
self.audio_player_state.volume * 100.0
|
||||||
|
));
|
||||||
|
|
||||||
|
let should_update_volume = !self.app_state.volume_dragged
|
||||||
|
&& self
|
||||||
|
.app_state
|
||||||
|
.ignore_volume_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_volume {
|
||||||
|
self.app_state.volume_slider_value = self.audio_player_state.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut self.app_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
let volume_slider_response = ui.add_sized([150.0, 18.0], volume_slider);
|
||||||
|
if volume_slider_response.drag_stopped() {
|
||||||
|
self.app_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
// ------------------------------------------
|
||||||
|
|
||||||
|
ui.add_space(ui.available_width() - 18.0 * 2.0 - ui.spacing().item_spacing.x * 2.0);
|
||||||
|
|
||||||
|
// ---------- Hotkeys button ----------
|
||||||
|
let hotkeys_button =
|
||||||
|
Button::new(ICON_KEYBOARD.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
||||||
|
let hotkeys_button_response = ui.add_sized([18.0, 18.0], hotkeys_button);
|
||||||
|
if hotkeys_button_response.clicked() {
|
||||||
|
self.app_state.show_hotkeys = true;
|
||||||
|
}
|
||||||
|
hotkeys_button_response.on_hover_text("Hotkeys (H)");
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
// ---------- Settings button ----------
|
||||||
|
let settings_button =
|
||||||
|
Button::new(ICON_SETTINGS.atom_size(Vec2::new(18.0, 18.0))).frame(false);
|
||||||
|
let settings_button_response = ui.add_sized([18.0, 18.0], settings_button);
|
||||||
|
if settings_button_response.clicked() {
|
||||||
|
self.app_state.show_settings = true;
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Button, CollapsingHeader, Color32, FontFamily, Label, RichText, Slider, Ui};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::{audio_player::TrackInfo, gui::AppState};
|
||||||
|
use pwsp::utils::gui::format_time_pair;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
pub(crate) enum TrackAction {
|
||||||
|
Pause(u32),
|
||||||
|
Resume(u32),
|
||||||
|
ToggleLoop(u32),
|
||||||
|
Stop(u32),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_header(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical_centered_justified(|ui| {
|
||||||
|
if self.audio_player_state.tracks.is_empty() {
|
||||||
|
ui.label("No tracks playing");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
for track in &self.audio_player_state.tracks {
|
||||||
|
CollapsingHeader::new(
|
||||||
|
RichText::new(
|
||||||
|
track
|
||||||
|
.path
|
||||||
|
.file_stem()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_str()
|
||||||
|
.unwrap_or_default(),
|
||||||
|
)
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.family(FontFamily::Monospace),
|
||||||
|
)
|
||||||
|
.default_open(true)
|
||||||
|
.show(ui, |ui| {
|
||||||
|
if let Some(act) = Self::draw_track_control(ui, &mut self.app_state, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.separator();
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
match action {
|
||||||
|
TrackAction::Pause(id) => self.pause(Some(id)),
|
||||||
|
TrackAction::Resume(id) => self.resume(Some(id)),
|
||||||
|
TrackAction::ToggleLoop(id) => self.toggle_loop(Some(id)),
|
||||||
|
TrackAction::Stop(id) => self.stop(Some(id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_playback_controls(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
let play_button = Button::new(if track.paused {
|
||||||
|
ICON_PLAY_ARROW
|
||||||
|
} else {
|
||||||
|
ICON_PAUSE
|
||||||
|
})
|
||||||
|
.corner_radius(15.0);
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], play_button).clicked() {
|
||||||
|
action = Some(if track.paused {
|
||||||
|
TrackAction::Resume(track.id)
|
||||||
|
} else {
|
||||||
|
TrackAction::Pause(track.id)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let loop_button = Button::new(
|
||||||
|
RichText::new(if track.looped {
|
||||||
|
ICON_REPEAT_ONE
|
||||||
|
} else {
|
||||||
|
ICON_REPEAT
|
||||||
|
})
|
||||||
|
.size(18.0),
|
||||||
|
)
|
||||||
|
.frame(false);
|
||||||
|
|
||||||
|
if ui.add_sized([15.0, 30.0], loop_button).clicked() {
|
||||||
|
action = Some(TrackAction::ToggleLoop(track.id));
|
||||||
|
}
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_position_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let duration = track.duration.unwrap_or(1.0);
|
||||||
|
let position_slider = Slider::new(&mut ui_state.position_slider_value, 0.0..=duration)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
let position_slider_width = ui.available_width()
|
||||||
|
- (30.0 * 3.0)
|
||||||
|
- default_slider_width
|
||||||
|
- (ui.spacing().item_spacing.x * 6.0);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = position_slider_width;
|
||||||
|
if ui.add_sized([30.0, 30.0], position_slider).drag_stopped() {
|
||||||
|
ui_state.position_dragged = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
let time_label =
|
||||||
|
Label::new(RichText::new(format_time_pair(track.position, duration)).monospace());
|
||||||
|
ui.add_sized([30.0, 30.0], time_label);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_volume_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
ui_state: &mut pwsp::types::gui::TrackUiState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
default_slider_width: f32,
|
||||||
|
) {
|
||||||
|
let volume_icon = Self::get_volume_icon(track.volume);
|
||||||
|
let volume_label = Label::new(RichText::new(volume_icon).size(18.0));
|
||||||
|
ui.add_sized([30.0, 30.0], volume_label)
|
||||||
|
.on_hover_text(format!("Volume: {:.0}%", track.volume * 100.0));
|
||||||
|
|
||||||
|
let volume_slider = Slider::new(&mut ui_state.volume_slider_value, 0.0..=1.0)
|
||||||
|
.show_value(false)
|
||||||
|
.step_by(0.01);
|
||||||
|
|
||||||
|
ui.spacing_mut().slider_width = default_slider_width - 30.0;
|
||||||
|
ui.spacing_mut().item_spacing.x = 0.0;
|
||||||
|
|
||||||
|
if ui.add_sized([30.0, 30.0], volume_slider).drag_stopped() {
|
||||||
|
ui_state.volume_dragged = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_stop_control(ui: &mut Ui, track: &TrackInfo) -> Option<TrackAction> {
|
||||||
|
let stop_button = Button::new(ICON_CLOSE).frame(false);
|
||||||
|
if ui.add_sized([30.0, 30.0], stop_button).clicked() {
|
||||||
|
Some(TrackAction::Stop(track.id))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_track_control(
|
||||||
|
ui: &mut Ui,
|
||||||
|
app_state: &mut AppState,
|
||||||
|
track: &TrackInfo,
|
||||||
|
) -> Option<TrackAction> {
|
||||||
|
let ui_state = app_state.track_ui_states.entry(track.id).or_default();
|
||||||
|
|
||||||
|
let should_update_position = !ui_state.position_dragged
|
||||||
|
&& ui_state
|
||||||
|
.ignore_position_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_position {
|
||||||
|
ui_state.position_slider_value = track.position;
|
||||||
|
}
|
||||||
|
|
||||||
|
let should_update_volume = !ui_state.volume_dragged
|
||||||
|
&& ui_state
|
||||||
|
.ignore_volume_update_until
|
||||||
|
.map(|t| Instant::now() > t)
|
||||||
|
.unwrap_or(true);
|
||||||
|
|
||||||
|
if should_update_volume {
|
||||||
|
ui_state.volume_slider_value = track.volume;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut action = None;
|
||||||
|
|
||||||
|
ui.horizontal_top(|ui| {
|
||||||
|
if let Some(act) = Self::draw_playback_controls(ui, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_slider_width = ui.spacing().slider_width;
|
||||||
|
Self::draw_position_control(ui, ui_state, track, default_slider_width);
|
||||||
|
Self::draw_volume_control(ui, ui_state, track, default_slider_width);
|
||||||
|
|
||||||
|
if let Some(act) = Self::draw_stop_control(ui, track) {
|
||||||
|
action = Some(act);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Color32, RichText, Ui};
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_hotkey_capture(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.add_space(ui.available_height() / 3.0);
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.capture.header"))
|
||||||
|
.size(18.0)
|
||||||
|
.color(Color32::YELLOW)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
ui.add_space(10.0);
|
||||||
|
let target = if let Some(slot) = &self.app_state.assigning_hotkey_slot {
|
||||||
|
format!("{} '{}'", t!("gui.hotkeys.capture.for"), slot)
|
||||||
|
} else if let Some(path) = &self.app_state.assigning_hotkey_for_file {
|
||||||
|
format!(
|
||||||
|
"{} '{}'",
|
||||||
|
t!("gui.hotkeys.capture.for"),
|
||||||
|
path.file_name().unwrap_or_default().to_string_lossy()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
String::new()
|
||||||
|
};
|
||||||
|
ui.label(RichText::new(target).size(16.0));
|
||||||
|
ui.add_space(10.0);
|
||||||
|
ui.label(t!("gui.hotkeys.capture.cancel"));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,311 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Button, Color32, Label, RichText, TextEdit, Ui};
|
||||||
|
use egui_extras::{Column, TableBuilder};
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
use pwsp::types::socket::Request;
|
||||||
|
use pwsp::utils::gui::make_request_async;
|
||||||
|
use rust_i18n::t;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
pub(crate) enum HotkeyAction {
|
||||||
|
Remove(String),
|
||||||
|
Capture(String),
|
||||||
|
ClearChord(String),
|
||||||
|
Play(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub fn draw_hotkeys(&mut self, ui: &mut Ui) {
|
||||||
|
ui.vertical(|ui| {
|
||||||
|
ui.spacing_mut().item_spacing.y = 5.0;
|
||||||
|
|
||||||
|
self.draw_hotkeys_header(ui);
|
||||||
|
ui.separator();
|
||||||
|
|
||||||
|
self.draw_hotkeys_search(ui);
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(5.0);
|
||||||
|
|
||||||
|
let action = self.draw_hotkeys_table(ui);
|
||||||
|
|
||||||
|
if let Some(action) = action {
|
||||||
|
self.handle_hotkey_action(action);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_header(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
let back_button = Button::new(ICON_ARROW_BACK).frame(false);
|
||||||
|
if ui.add(back_button).clicked() {
|
||||||
|
self.app_state.show_hotkeys = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.vertical_centered(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.header"))
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_search(&mut self, ui: &mut Ui) {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
ui.menu_button(
|
||||||
|
format!(
|
||||||
|
"{} {}",
|
||||||
|
ICON_ADD.codepoint,
|
||||||
|
t!("gui.hotkeys.add_command_select")
|
||||||
|
),
|
||||||
|
|ui| {
|
||||||
|
let mut selected_cmd = None;
|
||||||
|
if ui.button(t!("gui.hotkeys.toggle_pause_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_pause", Request::toggle_pause(None)));
|
||||||
|
}
|
||||||
|
if ui.button(t!("gui.hotkeys.stop_playback_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_stop", Request::stop(None)));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button(t!("gui.hotkeys.pause_playback_command"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
selected_cmd = Some(("cmd_pause", Request::pause(None)));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.button(t!("gui.hotkeys.resume_playback_command"))
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
selected_cmd = Some(("cmd_resume", Request::resume(None)));
|
||||||
|
}
|
||||||
|
if ui.button(t!("gui.hotkeys.toggle_loop_command")).clicked() {
|
||||||
|
selected_cmd = Some(("cmd_toggle_loop", Request::toggle_loop(None)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some((slot_name, req)) = selected_cmd {
|
||||||
|
make_request_async(Request::set_hotkey_action(slot_name, &req));
|
||||||
|
self.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.set_slot(slot_name.to_string(), req);
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot_name.to_string());
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
ui.close();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
ui.add_space(10.0);
|
||||||
|
|
||||||
|
ui.add(
|
||||||
|
TextEdit::singleline(&mut self.app_state.hotkey_search_query)
|
||||||
|
.hint_text(t!("gui.hotkeys.search_placeholder"))
|
||||||
|
.desired_width(f32::INFINITY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_hotkeys_table(&mut self, ui: &mut Ui) -> Option<HotkeyAction> {
|
||||||
|
let conflicts = self.app_state.hotkey_config.find_conflicts();
|
||||||
|
let conflict_slots: std::collections::HashSet<&str> =
|
||||||
|
conflicts.into_iter().flat_map(|(a, b)| [a, b]).collect();
|
||||||
|
|
||||||
|
let search = self.app_state.hotkey_search_query.to_lowercase();
|
||||||
|
let mut action: Option<HotkeyAction> = None;
|
||||||
|
|
||||||
|
let slots: Vec<_> = self
|
||||||
|
.app_state
|
||||||
|
.hotkey_config
|
||||||
|
.slots
|
||||||
|
.iter()
|
||||||
|
.filter(|s| {
|
||||||
|
if search.is_empty() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
s.slot.to_lowercase().contains(&search)
|
||||||
|
|| format!("{:?}", s.action).to_lowercase().contains(&search)
|
||||||
|
|| s.key_chord
|
||||||
|
.as_deref()
|
||||||
|
.unwrap_or("")
|
||||||
|
.to_lowercase()
|
||||||
|
.contains(&search)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let available_width = ui.available_width();
|
||||||
|
let col_width = (available_width / 4.0).max(80.0);
|
||||||
|
|
||||||
|
TableBuilder::new(ui)
|
||||||
|
.striped(true)
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Slot
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Sound / Action name
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Key Chord
|
||||||
|
.column(Column::exact(col_width).clip(true)) // Actions
|
||||||
|
.header(30.0, |mut header| {
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_slot"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_sound"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_key_chord"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
header.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.column_actions"))
|
||||||
|
.strong()
|
||||||
|
.monospace()
|
||||||
|
.color(Color32::LIGHT_GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.body(|mut body| {
|
||||||
|
if slots.is_empty() {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(t!("gui.hotkeys.no_hotkeys_configured"))
|
||||||
|
.color(Color32::GRAY),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
row.col(|_| {});
|
||||||
|
row.col(|_| {});
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for slot in &slots {
|
||||||
|
body.row(30.0, |mut row| {
|
||||||
|
// Column 1: Slot
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if conflict_slots.contains(slot.slot.as_str()) {
|
||||||
|
ui.label(
|
||||||
|
RichText::new(ICON_WARNING.codepoint)
|
||||||
|
.color(Color32::from_rgb(255, 165, 0)),
|
||||||
|
)
|
||||||
|
.on_hover_text("Key chord conflict");
|
||||||
|
}
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(&slot.slot).monospace()).truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 2: Sound / Action name
|
||||||
|
row.col(|ui| {
|
||||||
|
let action_name = match slot.action.name.as_str() {
|
||||||
|
"play" => {
|
||||||
|
if let Some(file_path_str) = slot.action.args.get("file_path") {
|
||||||
|
Path::new(file_path_str)
|
||||||
|
.file_name()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string()
|
||||||
|
} else {
|
||||||
|
"Play".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"toggle_pause" => "Toggle Pause".to_string(),
|
||||||
|
"pause" => "Pause Playback".to_string(),
|
||||||
|
"resume" => "Resume Playback".to_string(),
|
||||||
|
"stop" => "Stop Playback".to_string(),
|
||||||
|
"toggle_loop" => "Toggle Loop".to_string(),
|
||||||
|
other => other.to_string(),
|
||||||
|
};
|
||||||
|
ui.add(Label::new(RichText::new(action_name).monospace()).truncate());
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 3: Key Chord
|
||||||
|
row.col(|ui| {
|
||||||
|
let chord_text = slot.key_chord.as_deref().unwrap_or("(none)");
|
||||||
|
ui.add(
|
||||||
|
Label::new(RichText::new(chord_text).monospace().color(
|
||||||
|
if slot.key_chord.is_some() {
|
||||||
|
Color32::from_rgb(100, 200, 100)
|
||||||
|
} else {
|
||||||
|
Color32::GRAY
|
||||||
|
},
|
||||||
|
))
|
||||||
|
.truncate(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Column 4: Actions
|
||||||
|
row.col(|ui| {
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_DELETE).frame(false))
|
||||||
|
.on_hover_text("Remove slot")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Remove(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_KEYBOARD).frame(false))
|
||||||
|
.on_hover_text("Set key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Capture(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if slot.key_chord.is_some()
|
||||||
|
&& ui
|
||||||
|
.add(Button::new(ICON_BACKSPACE).frame(false))
|
||||||
|
.on_hover_text("Clear key chord")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::ClearChord(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
if ui
|
||||||
|
.add(Button::new(ICON_PLAY_ARROW).frame(false))
|
||||||
|
.on_hover_text("Play")
|
||||||
|
.clicked()
|
||||||
|
{
|
||||||
|
action = Some(HotkeyAction::Play(slot.slot.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_hotkey_action(&mut self, action: HotkeyAction) {
|
||||||
|
match action {
|
||||||
|
HotkeyAction::Remove(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey(&slot));
|
||||||
|
self.app_state.hotkey_config.remove_slot(&slot);
|
||||||
|
}
|
||||||
|
HotkeyAction::Capture(slot) => {
|
||||||
|
self.app_state.assigning_hotkey_slot = Some(slot);
|
||||||
|
self.app_state.hotkey_capture_active = true;
|
||||||
|
}
|
||||||
|
HotkeyAction::ClearChord(slot) => {
|
||||||
|
make_request_async(Request::clear_hotkey_key(&slot));
|
||||||
|
self.app_state.hotkey_config.set_key_chord(&slot, None);
|
||||||
|
}
|
||||||
|
HotkeyAction::Play(slot) => {
|
||||||
|
self.play_hotkey_slot(&slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::Ui;
|
||||||
|
use egui_material_icons::icons::*;
|
||||||
|
|
||||||
|
mod body;
|
||||||
|
mod footer;
|
||||||
|
mod header;
|
||||||
|
mod hotkey_capture;
|
||||||
|
mod hotkeys;
|
||||||
|
mod settings;
|
||||||
|
mod waiting_for_daemon;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
pub(crate) fn get_volume_icon(volume: f32) -> &'static str {
|
||||||
|
if volume > 0.7 {
|
||||||
|
ICON_VOLUME_UP.codepoint
|
||||||
|
} else if volume <= 0.0 {
|
||||||
|
ICON_VOLUME_OFF.codepoint
|
||||||
|
} else if volume < 0.3 {
|
||||||
|
ICON_VOLUME_MUTE.codepoint
|
||||||
|
} else {
|
||||||
|
ICON_VOLUME_DOWN.codepoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn draw(&mut self, ui: &mut Ui) {
|
||||||
|
self.draw_header(ui);
|
||||||
|
self.draw_body(ui);
|
||||||
|
ui.separator();
|
||||||
|
self.draw_footer(ui);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{Align, Button, Color32, Layout, RichText, Ui};
|
||||||
|
use egui_material_icons::icons::ICON_ARROW_BACK;
|
||||||
|
use rust_i18n::t;
|
||||||
|
|
||||||
|
impl SoundpadGui {
|
||||||
|
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(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(t!("gui.settings.header"))
|
||||||
|
.color(Color32::WHITE)
|
||||||
|
.monospace(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
ui.separator();
|
||||||
|
ui.add_space(20.0);
|
||||||
|
|
||||||
|
// --------- Checkboxes ----------
|
||||||
|
let save_volume_response = ui.checkbox(
|
||||||
|
&mut self.config.save_volume,
|
||||||
|
t!("gui.settings.remember_volume"),
|
||||||
|
);
|
||||||
|
let save_input_response =
|
||||||
|
ui.checkbox(&mut self.config.save_input, t!("gui.settings.remember_mic"));
|
||||||
|
let save_scale_response = ui.checkbox(
|
||||||
|
&mut self.config.save_scale_factor,
|
||||||
|
t!("gui.settings.remember_ui_scale"),
|
||||||
|
);
|
||||||
|
let pause_on_exit_response = ui.checkbox(
|
||||||
|
&mut self.config.pause_on_exit,
|
||||||
|
t!("gui.settings.pause_on_window_close"),
|
||||||
|
);
|
||||||
|
|
||||||
|
if save_volume_response.changed()
|
||||||
|
|| save_input_response.changed()
|
||||||
|
|| save_scale_response.changed()
|
||||||
|
|| pause_on_exit_response.changed()
|
||||||
|
{
|
||||||
|
self.config.save_to_file().ok();
|
||||||
|
}
|
||||||
|
// --------------------------------
|
||||||
|
|
||||||
|
ui.with_layout(Layout::bottom_up(Align::Min), |ui| {
|
||||||
|
ui.label(t!(
|
||||||
|
"gui.settings.version",
|
||||||
|
version = env!("CARGO_PKG_VERSION")
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
use crate::gui::SoundpadGui;
|
||||||
|
use egui::{RichText, Ui};
|
||||||
|
|
||||||
|
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(),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
+37
-7
@@ -5,9 +5,9 @@ use crate::types::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::{DirBuilderExt, MetadataExt, PermissionsExt};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::{error::Error, fs};
|
use std::{env, error::Error, fs};
|
||||||
use tokio::{
|
use tokio::{
|
||||||
io::{AsyncReadExt, AsyncWriteExt},
|
io::{AsyncReadExt, AsyncWriteExt},
|
||||||
net::UnixStream,
|
net::UnixStream,
|
||||||
@@ -37,22 +37,52 @@ pub fn get_daemon_config() -> DaemonConfig {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_current_uid() -> u32 {
|
||||||
|
rustix::process::geteuid().as_raw()
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_runtime_dir() -> PathBuf {
|
pub fn get_runtime_dir() -> PathBuf {
|
||||||
dirs::runtime_dir().unwrap_or(PathBuf::from("/run/pwsp"))
|
dirs::runtime_dir().unwrap_or_else(|| {
|
||||||
|
let uid = get_current_uid();
|
||||||
|
env::temp_dir().join(format!("pwsp-{}", uid))
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_runtime_dir() -> Result<()> {
|
pub fn create_runtime_dir() -> Result<()> {
|
||||||
let runtime_dir = get_runtime_dir();
|
let runtime_dir = get_runtime_dir();
|
||||||
if !runtime_dir.exists() {
|
|
||||||
fs::create_dir_all(&runtime_dir)?;
|
if runtime_dir.exists() {
|
||||||
|
let meta = fs::symlink_metadata(&runtime_dir)?;
|
||||||
|
if meta.is_symlink() {
|
||||||
|
return Err(anyhow::anyhow!("Runtime directory is a symlink"));
|
||||||
|
}
|
||||||
|
let uid = get_current_uid();
|
||||||
|
if meta.uid() != uid {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Runtime directory is owned by another user"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
if meta.permissions().mode() & 0o777 != 0o700 {
|
||||||
|
return Err(anyhow::anyhow!(
|
||||||
|
"Runtime directory has incorrect permissions"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fs::DirBuilder::new()
|
||||||
|
.recursive(true)
|
||||||
|
.mode(0o700)
|
||||||
|
.create(&runtime_dir)?;
|
||||||
}
|
}
|
||||||
fs::set_permissions(&runtime_dir, fs::Permissions::from_mode(0o700))?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_daemon_running() -> Result<bool> {
|
pub fn is_daemon_running() -> Result<bool> {
|
||||||
let lock_file = fs::File::create(get_runtime_dir().join("daemon.lock"))?;
|
let lock_file = fs::OpenOptions::new()
|
||||||
|
.create(true)
|
||||||
|
.write(true)
|
||||||
|
.truncate(false)
|
||||||
|
.open(get_runtime_dir().join("daemon.lock"))?;
|
||||||
match lock_file.try_lock() {
|
match lock_file.try_lock() {
|
||||||
Ok(_) => Ok(false),
|
Ok(_) => Ok(false),
|
||||||
Err(_) => Ok(true),
|
Err(_) => Ok(true),
|
||||||
|
|||||||
Reference in New Issue
Block a user